<![CDATA[fluffy.es - iOS development tutorials]]>https://fluffy.es/https://fluffy.es/favicon.pngfluffy.es - iOS development tutorialshttps://fluffy.es/Ghost 2.25Fri, 23 Oct 2026 14:53:19 GMT60<![CDATA[End of the road, and thanks for reading this blog!]]>TL;DR: I got demotivated from iOS development, and moved to web development instead, new blog at https://rubyyagi.com .

I have been writing on this iOS development blog for 2.5 years (start of 2018 until mid of 2020),  wrote 80+ articles and 52 Xcode tips in email! Thank

]]>
https://fluffy.es/goodbye/5fb1383d2b012b0b9547f6cfSun, 15 Nov 2020 14:17:28 GMT

TL;DR: I got demotivated from iOS development, and moved to web development instead, new blog at https://rubyyagi.com .

I have been writing on this iOS development blog for 2.5 years (start of 2018 until mid of 2020),  wrote 80+ articles and 52 Xcode tips in email! Thank you for reading this blog!

I started this blog after failing a job interview for an iOS developer position for a popular messaging app company, I was burnt out at previous iOS dev job and was thinking alternative way to make money aside from full time job.

I then came across 30x500 course by Amy Hoy and Alex Hillman (not affiliated, I just enrolled for their course), which taught about how to create products that people wants, by studying audiences behavior, helping them in public, and creating a product that addresses their pains. I was an iOS developer, so naturally I chose the audience of iOS developers as I might have more insight on the same field.

I had a few small iOS apps making coffee money (like few bucks a week, which is just enough to cover annual Apple developer fees) before starting this blog, and I wanted to give it a try on making product for iOS devs (as myself was an iOS dev). So I started writing articles and tutorials for iOS development stuff, helped answer question in iOS Dev Slack, StackOverflow and Reddit occassionally. I put a newsletter box below every post I wrote, and slowly more and more people subscribed to my newsletter, and it grew to 2500+ subscribers at current time of writing!

After writing a dozens of articles, I noticed a few common reoccuring pain points appearing in iOS dev subreddit and Slack, which revolve around not understanding how Auto Layout works, how Swift optionals work (the ?, ! was confusing to me at first too) and how to implement Sign in with Apple. I then wrote a book on these topics, and tried to market them to my newsletter subscribers, the sales was OK (wouldn't say its life changing, I still have a job lol).

As of today I have earned a total of $5,452 from all of my ebooks, in a span of two years since I first started selling my first book on Auto Layout. I have never made that much money from product income in my life, my apps was making a few hundred dollars a year at best, I have learned a ton on technical writing , marketing and most importantly understanding audience's pain (sounds like a cliche, I know).

I have also learned a lot more on iOS development by writing articles, my previous job mostly was to print JSON prettily using UIKit and I didn't get much exposure to other framework like CoreLocation, automated testing etc. In order to write about these topics, I had to do research on them and made a lot of demo apps to learn about them before writing article.

I have made friends with other iOS/macOS developers on Twitter, gotten offer to guest blog (Smashing Magazine, Bitrise) , and even gotten a few adhoc paid task from other iOS devs! All these wouldn't have happened without this blog, I am forever grateful for this.

Truth to be told, I was working as a Ruby/ Web developer while I was writing these iOS articles, and it's hard to churn out iOS articles regularly when you don't have frequent real life iOS development experiences.

I got demotivated slowly and eventually Apple introduced SwiftUI, which gotten a lot of attention of iOS devs. I can feel that people are slowly moving from UIKit to SwiftUI, it was a big shift in programming paradigm and at this point I lost interest on writing about iOS development.

I have since decided to switch from writing about iOS development to web development (mainly using Ruby and the framework Ruby on Rails) , if you are interested to follow along, you can check out https://rubyyagi.com , I would not update this blog anymore but I will keep it as is for archival purpose.

Thanks for reading and supporting this blog for the past 2 years!

Here's my books if you are interested :

  1. Making Sense of Auto Layout
  2. Understanding Optionals and Delegate
  3. Practical Sign in with Apple
]]>
<![CDATA[Toggle iCloud sync on/off for NSPersistentCloudKitContainer]]>

I wanted an option for user to toggle iCloud sync on / off for my own app AuthCat (2FA OTP app with iCloud sync).

icloud sync toggle

Some users may not feel comfortable sharing their data to cloud, or just prefer to not sync data between devices, so it is important to have an

]]>
https://fluffy.es/toggle-icloud-sync-nspersistentcloudkitcontainer/5f1ffa6a2b012b0b9547f67fTue, 28 Jul 2020 10:31:07 GMTToggle iCloud sync on/off for NSPersistentCloudKitContainer

I wanted an option for user to toggle iCloud sync on / off for my own app AuthCat (2FA OTP app with iCloud sync).

Toggle iCloud sync on/off for NSPersistentCloudKitContainer

Some users may not feel comfortable sharing their data to cloud, or just prefer to not sync data between devices, so it is important to have an option to let them disable it. My app default is iCloud sync off, they have to turn it on manually.

There's some NSPersistentCloudKitContainer tutorial online, but there isn't much guide on how to implement the sync toggling functionality, so I have decided to write this.

This tutorial assume that you already have a NSPersistentCloudKitContainer ready in your app. You can check Andrew's excellent tutorial on how to implement NSPersistentCloudKitContainer if you haven't already implement it.

Table of Contents :

  1. A boolean to store if the sync is on/off
  2. Disable cloud sync on NSPersistentCloudKitContainer
  3. Reinitialize the persistent container when iCloud sync is toggled
  4. Delete existing data from iCloud when iCloud sync is turned off

A boolean to store if the sync is on/off

First, you will need to have a boolean variable to store whether the user has turned the sync on or off, and this variable must be accessible by other iOS devices owned by the same Apple ID as well.

To make this boolean variable accessible on another iOS device, we need to store it using iCloud Key-Value store (NSUbiquitousKeyValueStore). You can think of it like UserDefaults, but the value is shared across different devices for the same app, that ties to an Apple ID account.

Enable the iCloud key-value storage capability in your project :

Toggle iCloud sync on/off for NSPersistentCloudKitContainer

Then in the UISwitch's IBAction, update the boolean value when user toggle the switch :

@IBAction func iCloudToggled(_ sender: UISwitch) {
    // set the icloud_sync key to be true/false depending on the UISwitch state
    NSUbiquitousKeyValueStore.default.set(sender.isOn, forKey: "icloud_sync")
}

Similar to UserDefaults, if the value for the key is not set previously, it will return false by default.

I have used the key name "icloud_sync" here, you can use other key name if you want to.

Disable cloud sync on NSPersistentCloudKitContainer

When you initialize a NSPersistentCloudKitContainer stack, by default it will sync to an iCloud container (the remote database), with the container identifier you specified in the capabilities section :

Toggle iCloud sync on/off for NSPersistentCloudKitContainer

If you print out the default value of the container identifier like this, you will get the same identifier shown in the capabilities section :

// your CoreData stack in AppDelegate.swift

lazy var persistentContainer: NSPersistentCloudKitContainer = {
  
  let container = NSPersistentCloudKitContainer(name: "AppName")

  guard let description = container.persistentStoreDescriptions.first else {
      fatalError("###\(#function): Failed to retrieve a persistent store description.")
  }
  
  // this will output "iCloud.es.fluffy.AuthCat"
  print("cloudkit container identifier : \(description.cloudKitContainerOptions?.containerIdentifier)")
  
  // ....
}

To disable iCloud syncing, we can set the cloudKitContainerOptions property to nil. By setting it to nil, the NSPersistentCloudKitContainer will not connect to any cloud container, hence no sync will occur.

We can then decide whether to set this property to nil based on the boolean we set earlier. Note that we need to set the cloudKitContainerOptions property to nil before we load the persistentStore.

// AppDelegate.swift
lazy var persistentContainer: NSPersistentCloudKitContainer = {
  let container = NSPersistentCloudKitContainer(name: "AppName")

  guard let description = container.persistentStoreDescriptions.first else {
      fatalError("###\(#function): Failed to retrieve a persistent store description.")
  }

  description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)

  description.setOption(true as NSNumber,
                              forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

  // if "icloud_sync" boolean key isn't set or isn't set to true, don't sync to iCloud
  if(!NSUbiquitousKeyValueStore.default.bool(forKey: "icloud_sync")){
      description.cloudKitContainerOptions = nil
  }

  container.loadPersistentStores(completionHandler: { (storeDescription, error) in
  })
  // ...
}

We have set "true" for the NSPersistentHistoryTrackingKey, so that when the user decide to turn on iCloud sync back, all the Core Data changes made during off-sync is tracked and synced to iCloud.

Reinitialize the persistent container when iCloud sync is toggled

When user toggle the iCloud sync switch, we need to update the cloudKitContainerOptions value and load the persistent store again to toggle iCloud sync. I find it easier to just reinitialize the whole persistent container variable.

I have extracted the previous NSPersistentCloudKitContainer code into a function  like this :

func setupContainer(withSync iCloudSync: Bool) -> NSPersistentCloudKitContainer{
    let container = NSPersistentCloudKitContainer(name: "AppName")
    
    guard let description = container.persistentStoreDescriptions.first else {
        fatalError("###\(#function): Failed to retrieve a persistent store description.")
    }
    
    description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
    
    description.setOption(true as NSNumber,
                          forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
    
    
    // if "cloud_sync" boolean key isn't set or isn't set to true, don't sync to iCloud
    if(!iCloudSync){
        description.cloudKitContainerOptions = nil
    }
    
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        // ...
    })

    // ...
    
    container.viewContext.automaticallyMergesChangesFromParent = true
    return container
}

Then when user toggle the iCloud sync switch, you can reinitialize the AppDelegate's persistentContainer variable like this :

@IBAction func iCloudToggled(_ sender: UISwitch) {
    // set the icloud_sync key to be true/false depending on the UISwitch state
    NSUbiquitousKeyValueStore.default.set(sender.isOn, forKey: "icloud_sync")

    // reinitialize the persistentContainer and re-load persistentStores
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    appDelegate.persistentContainer = setupContainer(withSync: sender.isOn)
}

Delete existing data from iCloud when iCloud sync is turned off

When user turn off  iCloud sync, they might expect that the data on iCloud server is deleted, for privacy reasons. I think we should respect user's decision to remove existing data from Apple's server.

When we use NSPersistentCloudKitContainer, Apple will create a special zone in iCloud named "com.apple.coredata.cloudkit.zone" to store the user's private data.

Toggle iCloud sync on/off for NSPersistentCloudKitContainer

To delete the existing data synced on iCloud, we can simply delete the whole zone like this :

// replace the identifier with your container identifier
let container = CKContainer(identifier: "iCloud.es.fluffy.AuthCat")

let database = container.privateCloudDatabase

// instruct iCloud to delete the whole zone (and all of its records)
database.delete(withRecordZoneID: .init(zoneName: "com.apple.coredata.cloudkit.zone"), completionHandler: { (zoneID, error) in
    if let error = error {
        print("deleting zone error \(error.localizedDescription)")
    }
})

Remember to replace the CKContainer identifier with your container identifier string.

Toggle iCloud sync on/off for NSPersistentCloudKitContainer

In the iCloud sync switch IBAction :

@IBAction func iCloudToggled(_ sender: UISwitch) {
    // set the icloud_sync key to be true/false depending on the UISwitch state
    NSUbiquitousKeyValueStore.default.set(sender.isOn, forKey: "icloud_sync")

    // reinitialize the persistentContainer and re-load persistentStores
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    appDelegate.persistentContainer = setupContainer(withSync: sender.isOn)
  
    // delete the zone in iCloud if user switch off iCloud sync
    if(!sender.isOn){
        // replace the identifier with your container identifier
        let container = CKContainer(identifier: "iCloud.es.fluffy.AuthCat")

        let database = container.privateCloudDatabase

        // instruct iCloud to delete the whole zone (and all of its records)
        database.delete(withRecordZoneID: .init(zoneName: "com.apple.coredata.cloudkit.zone"), completionHandler: { (zoneID, error) in
            if let error = error {
                print("deleting zone error \(error.localizedDescription)")
            }
        })
    }
}

]]>
<![CDATA[Read and validate in-app purchase receipt locally using TPInAppReceipt]]>https://fluffy.es/in-app-purchase-receipt-local/5f16bf002b012b0b9547f61fTue, 21 Jul 2020 10:45:00 GMTRead and validate in-app purchase receipt locally using TPInAppReceipt

Table of contents:

  1. Disclaimer and Caveats
  2. Prerequisite
  3. Installation
  4. Getting the receipt data
  5. Check if user has purchased products or has active subscription
  6. Check receipt original app version
  7. Validate receipt

Disclaimer and Caveats

This tutorial is aimed to get you on feet to validate and read the in-app purchase receipt data as easy as possible using a library, using library might make it easier for jailbreaker to retrieve your app's premium content for free.

The reason so many tutorials out there asks you to include OpenSSL library as static library in the bundle, is to make it harder for the hacker (user who jailbroken their device) to replace the library during run time with a hacked version to work around validation checks. For example, if the library has a function that checks for if the in-app purchase is bought ( func isPurchased() ), they can replace it with a hacked library that always return true for this function.

Most Stack Overflow answers and tutorials also advocate for rolling your own validation function, to make it harder for hacker to guess and modify them. If you are using an open source library on Github for receipt validation, the hacker can just simply read the source of the library and know which part they should modify to get your premium content for free.

My belief is that people who decide to pirate your app won't buy it anyway, and there's no stopping a determined hacker to disassemble your app and get your premium content for free, you can only make it harder for them to do so, but usually it is not worth the effort as the percentage of people who decide to hack your app is very small.

If you don't care about the potential piracy issue and just want a straightforward way to check if the user has purchased your in-app purchase and show the premium content to them, this tutorial is for you.

I practice what I preach, I am using TPInAppReceipt library on my own app AuthCat (Simple 2FA OTP app with iCloud sync) for reading and validating receipt.

Prerequisite

This tutorial assumes you

  1. already have a paid Apple developer account
  2. have created an app in App Store Connect
  3. have created the in-app purchase product in App Store Connect with status 'Ready to Submit'
  4. have implemented the in-app purchase flow in your app, as in you can purchase the product already

More info on how to troubleshoot your in-app purchase setup here.

Installation

We will be using the TPInAppReceipt library created by Pavel for reading and validating the in-app purchase receipt. Thanks Pavel!

Cocoapods

Add this line into your Podfile :

pod 'TPInAppReceipt'


then run pod install .

In any swift file you'd like to use TPInAppReceipt, import the framework using import TPInAppReceipt.

Carthage

Add this line into your Cartfile :

github "tikhop/TPInAppReceipt" 


then run carthage update .

Getting the receipt data

TPInAppReceipt library encapsulates the receipts data in an InAppReceipt object, you can retrieve the local receipt data using .localReceipt() method like this :

import InAppReceipt

if let receipt = try? InAppReceipt.localReceipt() {
  // do your validation or parsing here
} else {
  print("Receipt not found")
}

When a user downloads app from the App Store, App Store will generate an app receipt and bundle it with the app.

Read and validate in-app purchase receipt locally using TPInAppReceipt

We can access the receipt file using Bundle.main.appStoreReceiptURL , the URL string might look like this : YourAppPath/StoreKit/Receipt.  The InAppReceipt.localReceipt() is a function that wraps around the Bundle.main.appStoreReceiptURL and attempt to read the file located at the URL.

When we build the app using Xcode or download it using Testflight, usually the receipt is not included, and the localReceipt() method will throw an error saying receipt not found.

We can manually request a receipt from the sandbox App Store by calling the StoreKit function SKReceiptRefreshRequest.start() . (Read more here) . TPInAppReceipt library also provides a wrapper around this function, we can call InAppReceipt.refresh to retrieve or refresh the receipt file :

InAppReceipt.refresh { (error) in
  if let err = error
  {
    print(err)
  }else{
    // do your stuff with the receipt data here
    if let receipt = try? InAppReceipt.localReceipt() {
      // ...
    }
  }
}

Check if user has purchased products or has active subscription

Here's the fun part. For non-consumable product, you can check if the user has purchased it using containsPurchase(ofProductIdentifier) function :

if let receipt = try? InAppReceipt.localReceipt(){
    if receipt.containsPurchase(ofProductIdentifier: "your.product.identifier"){
        // user has purchased this product
        // return true
    }
}

For auto-renewable subscription, you can check if the user has any active subscriptions with hasActiveAutoRenewablePurchases property :

if let receipt = try? InAppReceipt.localReceipt(){
    if receipt.hasActiveAutoRenewablePurchases {
        // user has active subscription
        // return true
    }
}

If you would like to be more specific, you can check if user has an active subscription of a specific product ID, until a specified date like this :

if let receipt = try? InAppReceipt.localReceipt(){
    if receipt.hasActiveAutoRenewableSubscription(ofProductIdentifier: "your.product.identifier.", forDate: Date()) {
        // user has subscription of the product, which is still active at the specified date
        // return true
    }
}

No idea on how to implement code for handling in-app purchase?

Get my preset IAPStore helper code, which you can use it to allow user to buy IAP, restore IAP and check if user has purchased IAP easily.

https://gist.github.com/cupnoodle/63daf45c06aad525f81ddd195cb699fb

Check receipt original app version

If your app is a paid app and you want to make it free with in-app purchase, at the same time allowing user who have purchased the paid app previously to get the in-app purchase for free, you can use the originalAppVersion property to check what is the app version number when the user first downloaded the app.

According to Apple's documentation,  originalAppVersion refers to the build number of your iOS app :

Read and validate in-app purchase receipt locally using TPInAppReceipt
Read and validate in-app purchase receipt locally using TPInAppReceipt

If the original app version number is smaller than the build number that was last used when the app is still paid, grant the user access to the in-app purchase.

// cast the string into integer (build number)
guard let receipt = try? InAppReceipt.localReceipt(),
      let originalBuildNumber = Int(receipt.originalAppVersion) else {
    return
}

// the last build number that the app is still a paid app
if originalBuildNumber < 37 {
    // grant user premium feature here
    print("premium granted")
}


Validate receipt

TPInAppReceipt also provides methods to verify if the receipt is legit and not forged. You can verify the receipt's :

  1. Bundle Identifier and Version
  2. GUID Hash
  3. Signature
// Verify GUID hash 
try? receipt.verifyHash()

// Verify bundle identifier and version
try? receipt.verifyBundleIdentifierAndVersion()

/// Verify signature
try? receipt.verifySignature()

You can verify these three steps at once using the verify() method :

// Verify all at once

do {
    try receipt.verify()
} catch IARError.validationFailed(reason: .hashValidation) 
{
    // Hash validation failed
} catch IARError.validationFailed(reason: .bundleIdentifierVefirication) 
{
    // Bundle identifier verification failed
} catch IARError.validationFailed(reason: .signatureValidation) 
{
    // Signature validation
} catch {
    // Miscellaneous error
}

objc.io has written an excellent article which explains these validation steps in detail here : https://www.objc.io/issues/17-security/receipt-validation/ , I highly recommend giving it a read even though you are using a library to handle it.

Here are some key points from objc.io article on receipt :

A receipt is created and signed by Apple through the App Store.
A receipt is issued for a specific version of an application and a specific device.
A receipt is stored locally on the device.
A receipt is issued each time an installation or an update occurs.
When an application is installed, a receipt that matches the application and the device is issued.
When an application is updated, a receipt that matches the new version of the application is issued.
A receipt is issued each time a transaction occurs:
When an in-app purchase occurs, a receipt is issued so that it can be accessed to verify that purchase.
When previous transactions are restored, a receipt is issued so that it can be accessed to verify those purchases.

Below I will explain each of the verification steps in simplified form.

Bundle Identifier and Version

When a receipt is issued from Apple and stored into your app bundle, the receipt contains data about the bundle identifier and bundle version (on iOS, the bundle version refers to the CFBundleVersion, which is the build number value).

The bundle identifier value should match your app bundle identifier, and the bundle version should match the build number value of the app. This verification step is to prevent the hacker to take a valid receipt from another app and replace it in your app bundle, thus bypassing the check if you didn't implement this step.

Here's an excerpt from the .verifyBundleIdentifierAndVersion() method :

// check if bundle identifier value of the receipt equal to the app ones
guard let bid = Bundle.main.bundleIdentifier, bid == bundleIdentifier else
{
    throw IARError.validationFailed(reason: .bundleIdentifierVefirication)
}

// check if bundle version value of the receipt equal to the app ones
guard let v = Bundle.main.infoDictionary?["CFBundleVersion"] as? String,
            v == appVersion else
{
    throw IARError.validationFailed(reason: .bundleVersionVefirication)
}

GUID Hash

GUID (or UUID) is an acronym for 'Globally Unique Identifier' (or 'Universally Unique Identifier').

On iOS, this identifier is generated using UIDevice.current.identifierForVendor.uuid.

Here is the simplified diagram on how the hash comparison works :

Read and validate in-app purchase receipt locally using TPInAppReceipt

The receipt contains data of the app's bundle identifier, an opaque value (you can think of it like a salt before hashing), and the resulting SHA1 hash value, which you can compare with later.

We first generate the Device GUID using UIDevice's identifierForVendor.uuid , then concatenate the GUID value with the Opaque value, then concatenate with the Bundle Identifier.

Then we take the concatenated value and generate a SHA1 hash, then we compare this generated hash with the SHA1 hash value that is present in the receipt data. This two value should be equal, if they are not equal, this means that the receipt is not valid (which might be forged by the hacker).

Signature

A legitimate receipt is signed by Apple, using their own private key (which only Apple have it).

The local receipt container (TPInAppReceipt.localReceipt()) contains the receipt data, signature and also a set of public certificates :

Read and validate in-app purchase receipt locally using TPInAppReceipt

The signature is generated by Apple, by using their iTunes App Store private key to encrypt the hash generated from Receipt data.

Read and validate in-app purchase receipt locally using TPInAppReceipt

Only Apple holds the iTunes App Store private key, hence only them can generate the correct signature.

We then use the public certificate provided to decrypt the signature to generate the hash, and make sure this hash matches the receipt data hash. If it matches, it means the receipt indeed does comes from Apple.

Read and validate in-app purchase receipt locally using TPInAppReceipt

A determined hacker could forge a fake receipt with fake certificates and signature inside, which might bypass the validation process above. To ensure the legitimacy of the public certificates, we can download the trusted root Apple CA certificate from Apple website, and include it in our app, then use it to check if the iTunes App Store Certificate and WorldWide Developer (WWDR) Certificate is trusted.

Apple uses Chain of Trust method to grant signing privilege to other Non root certificate. When a certificate signs another certificate, it will generate a signature on the signed certificate, we then can trace back and use this signature to check if this certificate is actually signed by a trusted, higher authority certificate.

Read and validate in-app purchase receipt locally using TPInAppReceipt

TPInAppReceipt library has included the Apple Root CA certificate in the library, then it will use this certificate to check if the WorldWide Developer Certificate and iTunes App Store Certificate is within the chain of trust.

I have contributed the chain of trust validation and signature verification part for the TPInAppReceipt library, you can read more on the pull request here : https://github.com/tikhop/TPInAppReceipt/pull/37/files

No idea on how to implement code for handling in-app purchase?

Get my preset IAPStore helper code, which you can use it to allow user to buy IAP, restore IAP and check if user has purchased IAP easily.

https://gist.github.com/cupnoodle/63daf45c06aad525f81ddd195cb699fb

]]>
<![CDATA[Use Xcode Previews with UIKit]]>

Prerequisite: You will need macOS Catalina (10.15)+ and Xcode 11+ to run Xcode Previews.

demo

Apple introduced Xcode Previews in WWDC 2019 alongside with SwiftUI, which allow us to view UI changes immediately after each change, instead of needing to recompile the app after each UI changes.

You might think

]]>
https://fluffy.es/xcode-previews-uikit/5edcef8f2b012b0b9547f5beSun, 07 Jun 2020 15:20:51 GMTUse Xcode Previews with UIKit

Prerequisite: You will need macOS Catalina (10.15)+ and Xcode 11+ to run Xcode Previews.

Use Xcode Previews with UIKit

Apple introduced Xcode Previews in WWDC 2019 alongside with SwiftUI, which allow us to view UI changes immediately after each change, instead of needing to recompile the app after each UI changes.

You might think that Xcode Previews only works for SwiftUI project, but it can work on UIKit's UIViewController and UIView too! This makes coding UI programmatically a lot more attractive now with (almost) instant preview, no need to keep build and run on each changes, and you don't have to deal with slow IBDesignables in Storyboard anymore.

This article assumes that your iOS app supports iOS 12 and older, you can remove the *@available(iOS 13, ) line if your app only supports iOS 13 and newer.

Table of contents:

  1. How Previews work
  2. Wrapping UIViewController inside SwiftUI View
  3. UIViewControllerRepresentable extension
  4. Previewing multiple devices
  5. Live Preview
  6. Objective-C

How Previews work

To enable preview for a SwiftUI view, we need to create a struct which conform to the PreviewProvider protocol like this :

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        // the SwiftUI View
        ContentView()
    }
}

When you create a new SwiftUI View, Xcode will auto generate this block at the bottom of the View code for you.

Since we are using UIKit's view, we need to wrap our UIKit View Controller inside a SwiftUI View to make it work, using UIViewControllerRepresentable protocol, or UIViewRepresentable if you are using UIView instead of UIViewController.

Wrapping UIViewController inside SwiftUI View

Say you have a View Controller like this :

// InfoViewController.swift

import UIKit

class InfoViewController: UIViewController {
  ...
}

We can create a struct which conform to UIViewControllerRepresentable, and return this view controller in the makeUIViewController(context: Context) method.  This will wrap the UIViewController in a SwiftUI View.

We can leave the updateUIViewController function empty, as we don't have SwiftUI state or binding that will update the view controller in this case.

Next, we will use this struct inside another struct which conforms PreviewProvider protocol, to generate the preview :

// InfoViewController.swift

import UIKit

class InfoViewController: UIViewController {
  ...
}

#if DEBUG
import SwiftUI

struct InfoVCRepresentable: UIViewControllerRepresentable {
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        // leave this empty
    }
    
    @available(iOS 13.0.0, *)
    func makeUIViewController(context: Context) -> UIViewController {
        InfoViewController()
    }
}

@available(iOS 13.0, *)
struct InfoVCPreview: PreviewProvider {
    static var previews: some View {
       InfoVCRepresentable()
    }
}
#endif

With UIViewControllerRepresentable and PreviewProvider implemented, we now can view the preview. If the preview pane doesn't show, press option + command + enter (or from the top menu Editor > Canvas) to open the preview pane.

Use Xcode Previews with UIKit

Now when you make any changes to the view controller code, you can see it reflect (almost) immediately on the preview! 🚀

The #if DEBUG and #endif macro means that the code in between them will only be included in the compiled app if the app is built for debugging purpose (eg: when built from Xcode to simulator or your phone), as we don't need the preview code in our production app (in App Store).

It can be repetitive to implement the UIViewControllerRepresentable struct for each of the View Controllers, we can create an extension for this to eliminate this repeating work.

UIViewControllerRepresentable extension

Let's create new Swift File for this extension :

Use Xcode Previews with UIKit
Use Xcode Previews with UIKit

Name it as "UIViewController+Preview.swift".

Use Xcode Previews with UIKit

Then put in the extension code like below :

//UIViewController+Preview.swift

import UIKit

#if DEBUG
import SwiftUI

@available(iOS 13, *)
extension UIViewController {
    private struct Preview: UIViewControllerRepresentable {
        // this variable is used for injecting the current view controller
        let viewController: UIViewController

        func makeUIViewController(context: Context) -> UIViewController {
            return viewController
        }

        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        }
    }

    func toPreview() -> some View {
        // inject self (the current view controller) for the preview
        Preview(viewController: self)
    }
}
#endif

With this extension, we just need to add the PreviewProvider confirming struct below each view controllers, and use ViewController.toPreview() for the preview view.

// InfoViewController.swift

import UIKit

class InfoViewController: UIViewController {
  ...
}

#if DEBUG
import SwiftUI

@available(iOS 13, *)
struct InfoVCPreview: PreviewProvider {
    
    static var previews: some View {
        // view controller using programmatic UI
        InfoViewController().toPreview()
    }
}
#endif

If you are using Storyboard for the view controller, you can instantiate it using the Storyboard ID.

Ensure that you have set a unique storyboard ID for your view controller :

Use Xcode Previews with UIKit

Then in the PreviewProvider, instantiate the view controller from the storyboard :

#if DEBUG
import SwiftUI

@available(iOS 13, *)
struct ProfileVCPreview: PreviewProvider {
    static var previews: some View {
        // Assuming your storyboard file name is "Main" 
        UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "ProfileViewController").toPreview()
    }
}
#endif

Previewing multiple devices

It would be handy if we can preview how our UI looks like in multiple devices at once. We can specify an array of devices and create preview in each of the device like this :

#if DEBUG
import SwiftUI

@available(iOS 13, *)
struct InfoVCPreview: PreviewProvider {
    static var devices = ["iPhone SE", "iPhone 11 Pro Max"]
    
    static var previews: some View {
        ForEach(devices, id: \.self) { deviceName in
            InfoViewController().toPreview().previewDevice(PreviewDevice(rawValue: deviceName))
            .previewDisplayName(deviceName)
        }
    }
}
#endif

Use Xcode Previews with UIKit

The ForEach loop will loop through each of the device specified in the devices array, deviceName is the value of the element used in the loop (eg: iPhone SE).

.previewDevice() will show the UI in the Preview Device specified, which is created in the PreviewDevice(rawValue: ) function.

.previewDisplayName() is the text that will be displayed below the device in the preview, we'll use the device name for this.

I usually use the smallest device (iPhone SE) and largest device (iPhone 11 Pro Max) for previewing UI. If the UI works on both smallest and largest devices, it most likely will look ok across all devices.

You can specify different devices like "iPhone 8", "iPhone 8 Plus", "iPad Pro (11-inch)" etc. Here is the list of supported device in Preview currently :

// The following values are supported:
//
//     "iPhone 8"
//     "iPhone 8 Plus"
//     "iPhone SE"
//     "iPhone 11"
//     "iPhone 11 Pro"
//     "iPhone 11 Pro Max"
//     "iPad mini 4"
//     "iPad Air 2"
//     "iPad Pro (9.7-inch)"
//     "iPad Pro (12.9-inch)"
//     "iPad (5th generation)"
//     "iPad Pro (12.9-inch) (2nd generation)"
//     "iPad Pro (10.5-inch)"
//     "iPad (6th generation)"
//     "iPad Pro (11-inch)"
//     "iPad Pro (12.9-inch) (3rd generation)"
//     "iPad mini (5th generation)"
//     "iPad Air (3rd generation)"
//     "Apple TV"
//     "Apple TV 4K"
//     "Apple TV 4K (at 1080p)"
//     "Apple Watch Series 2 - 38mm"
//     "Apple Watch Series 2 - 42mm"
//     "Apple Watch Series 3 - 38mm"
//     "Apple Watch Series 3 - 42mm"
//     "Apple Watch Series 4 - 40mm"
//     "Apple Watch Series 4 - 44mm"

Live Preview

Xcode Previews only loads the initial UI by default. If you want to interact with the view controller , execute code and update the UI like in simulator, you can utilize the Live Preview button :

Use Xcode Previews with UIKit
Use Xcode Previews with UIKit

Objective-C

Xcode Previews works with Objective-C too! For simplicity, I prefer creating a new Swift file to generate the preview for the Objective-C view controller.

This section assumes you have created an extension for the ViewController UIViewControllerRepresentable as shown above (UIViewController+Preview.swift).

Create a new Swift file, I prefer to name it as "(ViewControllerName)Preview.swift", then fill in the PreviewProvider struct details :

//DerpViewControllerPreview.swift

// DerpViewController is an Objective-C View Controller

#if DEBUG
import SwiftUI

@available(iOS 13, *)
struct DerpVCPreview: PreviewProvider {
    static var devices = ["iPhone SE", "iPhone 11 Pro Max"]
    
    static var previews: some View {
        ForEach(devices, id: \.self) { deviceName in
            DerpViewController().toPreview().previewDevice(PreviewDevice(rawValue: deviceName))
            .previewDisplayName(deviceName)
        }
    }
}
#endif

Update bridging header file and import the view controller :

// ProjectName-Bridging-Header.h

//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "DerpViewController.h"

Open the Preview swift file you just created, a canvas should show up (press option + command + enter if it doesn't show), you can then open another tab for the Objective-C view controller to edit it.

And now you can make change on your Objective-C view controller and preview it!

Use Xcode Previews with UIKit
]]>
<![CDATA[3 steps to speed up storyboard]]>https://fluffy.es/3-steps-to-speed-up-storyboard/5ec8fd3a2b012b0b9547f593Sat, 23 May 2020 10:45:09 GMT3 steps to speed up storyboard

When you are working on storyboard with many view controllers, especially with IBDesignable UI, it can take quite a long while to open storyboard, and then you hear your Mac fan spins like it is about to take off 🚀 😂.

3 steps to speed up storyboard

Here's 3 steps you can take to speed up the storyboard.

1. Remove the use of IBDesignable View if possible

IBDesignable is great for showing custom view attributes lilke cornerRadius, shadow on the storyboard without having to build and run the app. However Xcode seems to use a lot of resource on rendering IBDesignable, and sometimes it will automatically build when you are just trying to open Storyboard.

If possible, try to remove IBDesignable view on your storyboard's view controller, and initialize it in your code instead.

2. Uncheck automatically refresh views

If it is not possible to remove IBDesignable View, and you find that Xcode keeps building automatically after each key press, you can try uncheck "Automatically Refresh Views".

Open your storyboard (make sure it is in the active tab), then uncheck Editor > Automatically Refresh Views.

3 steps to speed up storyboard

This will stop Xcode from automatically building the storyboard every time you change your code.

You can enable it back as needed when you are done with your code, to see the updated visual preview.

Alternatively, you can keep it off, and click "Refresh All Views" in the Editor menu, to refresh the view manually.

3 steps to speed up storyboard

3. Use multiple storyboard files and storyboard references

If you stuff all view controllers into one storyboard, it will get slow eventually. I suggest one storyboard should have less than 15 view controllers.

You can select a few related view controllers, usually inside the same navigation controller, then select Editor > Refactor to Storyboard .

3 steps to speed up storyboard

Xcode will then create a new storyboard file for you, and also a storyboard reference from the previous controller. The storyboard reference will tell the previous controller to continue the segue in the newly created storyboard.

3 steps to speed up storyboard

Usually I will refactor view controllers inside the same navigation controller or tab bar controller into a new storyboard, and name the storyboard based on the tab name or the flow name of the navigation controller (eg: user registration ).

With multiple storyboards, different dev in the team can work on different storyboard and avoid merge conflict when merging different feature branch.

Storyboard is just a XML file underneath, parsing XML files and generating layout from it can be time consuming as the XML file grows larger (with addition of more views). It is best to keep each storyboard files small.

Or alternatively you can say "screw it", and go for coding the user interface programmatically 😂, I recommend watching Brian's Let's Build That App series on building UI programmatically. For small personal projects I would still use Storyboard / XIBs as it is easier to visualize the design, and size classes.

]]>
<![CDATA[What is Delegate? Understand it by building your own]]>https://fluffy.es/swift-delegate/5ebbcb9a2b012b0b9547f551Wed, 13 May 2020 10:47:04 GMTWhat is Delegate? Understand it by building your own

In this tutorial, we are going to replicate tableview and its dataSource and delegate properties using a vertical stackview like this :

What is Delegate? Understand it by building your own

We will implement custom functions like numOfRows, textAtRow and buttonTappedAtRow for the stack view, which mimics tableview's numberOfRowsInSection, cellForRowAt, didSelectRowAt functions.

By the end of this tutorial, you will learn how delegate works, and have some understanding on how tableview datasource and delegate works.

Before moving on, I recommend you to download the starter project, which contain the Textfield, segmented control and the blank stack view prepared, then you can follow along with this tutorial easily.

Table of contents :

  1. Introduction
  2. Recap on Protocol
  3. Implementing dataSource
  4. Implementing delegate

Introduction

If you haven't grasp the concept of Optionals (eg: ?, !) yet, I recommend you to read the Optionals article first before reading this article. You will need to understand what does "?" mean for the paragraphs below.

Delegate pattern is very common in Apple's code, you can see it in tableview, collection view, URLSession etc.

For example in table view, you can set dataSource and delegate of table view to a view controller like this :

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    tableView.dataSource = self
    tableView.delegate = self
  }
}


Recap on Protocol

Before explaining delegate, we first need to understand the concept of protocol . In Swift, we can define a protocol like this :

protocol Animal {
  func makeNoise()
}


Protocol specifies what methods (and/or variables) must be implemented, in the Animal protocol, there's makeNoise() method. We can conform a class to a protocol like this :

// Example below means the class Cat conforms to the Animal
// protocol, not that Cat is a subclass of Animal
class Cat : Animal {
  func makeNoise(){
    print("meow")
  }
}


Here we have declared a class Cat, and make it conform to the Animal protocol (by putting ": Animal" after the class name). When a class conforms to a protocol, it must implement all the methods mentioned in the protocol, else we will get an error like this :

What is Delegate? Understand it by building your own

We can fix the error by implementing the makeNoise() method in the Cat class as shown previously.

This is the same when you conform your view controller to a UITableViewDataSource protocol, if you didn't implement numberOfRowsInSection and cellForRowAt indexpath method, you'll get the same error :

What is Delegate? Understand it by building your own

Here's a simplified view of UITableViewDataSource's protocol :

protocol UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
  
  // ...
}


Now you know that the basics of protocol, you might ask "whats the purpose of using protocol?" , "why do we have to force a class to implement some specified methods?" .

The purpose of protocol is abstraction, if a class conform to a certain protocol, we know that the class will surely implemented the methods specified in the protocol. This allows us to call those methods in the protocol from the class safely without knowing what exactly is the class type.

Say we have classes Cat, Dog and Fox which conform to Animal protocol :

class Cat : Animal {
  func makeNoise(){
    print("meow")
  }
}

class Dog : Animal {
  func makeNoise(){
    print("woof")
  }
}

class Fox : Animal {
  func makeNoise(){
    print("wa pow pow pow")
  }
}


We can then safely call makeNoise() for all of them :

let cat = Cat()
let dog = Dog()
let fox = Fox()

cat.makeNoise() // outputs "meow"
dog.makeNoise() // outputs "woof"
fox.makeNoise() // outputs "wa pow pow pow"

// can even put them in an array and loop like this 

// an array of objects which conforms to Animal protocol
let animals: [Animal] = [cat, dog, fox]

for animal in animals {
    animal.makeNoise()
}
// outputs:
// "meow"
// "woof"
// "wa pow pow pow"


This is similar to when we use the UITableViewDataSource protocol on different UIViewController classes. You can have LoginViewController, ProductListViewController etc, as long as your view controller implement  the function numberOfRowsAtSection, the tableView can access it by calling yourViewController.numberOfRowsAtSection() .

Next, we will look into how to implement the dataSource part of table view into our custom stack view.

Implementing dataSource

Tableview's dataSource is also using the same delegate concept, just different naming on the variable name.

Open the starter project, navigate to ViewController.swift, and then define a protocol for the data source, lets name it CustomStackViewDataSource, place it at the top of the file, like this :

// ViewController.swift

protocol CustomStackViewDataSource : AnyObject {
    func textForRowAt(index: Int) -> String
    func numberOfRows() -> Int
}

class CustomStackView : UIStackView {
  // ...
}

class ViewController: UIViewController {
  // ...
}


We have also set the function textForRowAt and numberOfRows for the data source protocol, these function are used by the stack view to check how many rows to create and what text to use for different rows.

The AnyObject means that only a class can conform to this protocol, struct can't conform to this protocol.

Next, we add a dataSource variable to the Custom Stack View :

class CustomStackView : UIStackView {
  // always set dataSource / delegate to weak to avoid reference cycle
  weak var dataSource : CustomStackViewDataSource?
  
  // ...
}


This variable must conform to the CustomStackViewDataSource protocol, which means it must implement the two functions we defined in the protocol.

We have set the dataSource variable to weak, to avoid reference cycle, and weak variable must be an optional since it can be nil.

Then in the View Controller's viewDidLoad() function, we set the dataSource of the stack view to self.

class ViewController: UIViewController {
  // ...
  override func viewDidLoad() {
      super.viewDidLoad()
      stackView.spacing = 20.0
    
      // set the dataSource to self, which is ViewController
      stackView.dataSource = self
  }
}


Xcode will show you an error "Cannot assign value of type 'ViewController' to type 'CustomStackViewDataSource?'", because we have set the stack view dataSource to the view controller, but the view controller haven't conform to the CustomStackViewDataSource protocol yet.

Let's conform the ViewController class to the protocol like this :

class ViewController: UIViewController, CustomStackViewDataSource {
	// ...
  override func viewDidLoad() {
    //...
  }
  
  // MARK: CustomStackViewDataSource
    func textForRowAt(index: Int) -> String {
        // if the textField is empty, return default text
        guard let text = textField.text, !text.isEmpty else {
            return "Default text"
        }
        
        // else return the text field text + the row number
        return text + " \(index)"
    }
    
    func numberOfRows() -> Int {
        // return the selected index of the segmented control + 1
        // if user select first index, index = 0, 
        // num of rows = 0 + 1 = 1
        return self.numberOfRowsSegment.selectedSegmentIndex  + 1
    }
}


We also set the value for textForRowAt and numberOfRows using the value from the TextField and selected index from the segmented control.

Next, we will implement the reloadData() function of the custom stack view, to make it behave similar to the tableView reloadData() function.

class CustomStackView : UIStackView {
  weak var dataSource : CustomStackViewDataSource?
  func reloadData() {
    guard let dataSource = dataSource else {
      return
    }
    
    // remove all the child view inside the stackview
    for subview in self.subviews {
      subview.removeFromSuperview()
    }
    
    // add buttons into the stack view, using the numberOfRows
    for i in 0...(dataSource.numberOfRows() - 1) {
      let button = UIButton()
      button.setTitle(dataSource.textForRowAt(index: i), for: .normal)
      button.setTitleColor(.systemBlue, for: .normal)
      button.setTitleColor(.systemPurple, for: .highlighted)
            
      self.addArrangedSubview(button)
    }
  }


Now build and run the app, type in some value in the textfield, and select a number from the segmented control, and tap "Update Stack View". You will see buttons will be added into the stack view :

What is Delegate? Understand it by building your own

How does this work?

How does the data source and protocol thingy work? You might ask.

When you set the stack view's dataSource variable to self in the view controller, it means that the stack view's dataSource variable is the view controller.

What is Delegate? Understand it by building your own

Thus the dataSource of the stack view refers to ViewController.

What is Delegate? Understand it by building your own

When calling the dataSource.numberOfRows()  inside the reloadData method, it is now referrring to ViewController.numberOfRows(), as dataSource is equal to ViewController.

What is Delegate? Understand it by building your own

Here's the flow of execution :

What is Delegate? Understand it by building your own

This is why the ViewController has to conform to CustomStackViewDataSource protocol, and implement the numberOfRows() method, as the numberOfRows() method will be called on it.

Similarly, textForRowAt() method has the same flow too.

What is Delegate? Understand it by building your own

Table view datasource works similarly, when you call tableView.reloadData(),  table view will call the data source (usually the view controller) 's numberOfRowsInSection and cellForRowAt function to retrieve how many rows the table view should have and the cell content for the row.

Implementing delegate

Next, we are going to implement delegate to detect action when the button in the stack view is tapped.

Similar to CustomStackViewDataSource, we will create another protocol for the delegate like this :

// ViewController.swift

protocol CustomStackViewDataSource : AnyObject {
    func textForRowAt(index: Int) -> String
    func numberOfRows() -> Int
}

// add this new delegate protocol
protocol CustomStackViewDelegate : AnyObject {
    func buttonTappedAt(index: Int)
}

class CustomStackView : UIStackView {
  // ...
}

class ViewController: UIViewController {
  // ...
}


The buttonTappedAt(index:) function will be called when we tap on one of the buttons in the stack view, with the index parameter indicating which row of the button we have tapped.

Next, in the CustomStackView class, we add another variable to be the CustomStackViewDelegate :

class CustomStackView : UIStackView {
    
    weak var dataSource : CustomStackViewDataSource?
    weak var delegate : CustomStackViewDelegate?
  
   // ....
}


Then inside the reloadData() function of the CustomStackView, we want to add action for the button we insert into the stack view. So that when they are tapped, the buttonTapped() function will be executed.

// inside class CustomStackView
func reloadData() {
    guard let dataSource = dataSource else {
      return
    }

    // remove all the child view inside the stackview
    for subview in self.subviews {
      subview.removeFromSuperview()
    }

    // add buttons into the stack view, using the numberOfRows
    for i in 0...(dataSource.numberOfRows() - 1) {

      let button = UIButton()

      button.setTitle(dataSource.textForRowAt(index: i), for: .normal)

      button.setTitleColor(.systemBlue, for: .normal)
      button.setTitleColor(.systemPurple, for: .highlighted)
      
      // execute the "buttonTapped" function declared below when the button is tapped.
      button.addTarget(self, action: #selector(buttonTapped(sender:)), for: .touchUpInside)

      self.addArrangedSubview(button)
    }
}

@objc func buttonTapped(sender: Any){

}


What is Delegate? Understand it by building your own

The current buttonTapped function doesn't do anything, we will come back to implement it later. Now,  we will set the stack view delegate to view controller, and implement the buttonTappedAt(index:) function.

class ViewController: UIViewController {
  // ...
  override func viewDidLoad() {
      super.viewDidLoad()
        
      stackView.spacing = 20.0
      stackView.dataSource = self
    
      // set the delegate to self, which is ViewController
      stackView.delegate = self
  }
}

At this point, Xcode will show you an error because your view controller haven't conform to the CustomStackViewDelegate protocol yet. To solve this, we will conform the view controller to the protocol and implement the delegate function :

class ViewController: UIViewController, CustomStackViewDataSource, CustomStackViewDelegate {
	// ...
  override func viewDidLoad() {
    //...
  }
  // MARK: CustomStackViewDataSource
  // ...

  // MARK: CustomStackViewDelegate
  func buttonTappedAt(index: Int) {
      print("tapped at \(index)")
  }
}


Now that we have implemented the buttonTappedAt protocol method in ViewController, let's move back to the buttonTapped() function the of CustomStackView, and implement the buttonTapped(sender:) function.

class CustomStackView : UIStackView {
  // ....
  @objc func buttonTapped(sender: Any){
        // ensure the sender (the UI that was tapped) is a button
        guard let button = sender as? UIButton else {
            return
        }
        
        // get the index of the button tapped from the stack view
        // buttons are subviews of the stack view, from top to bottom
        // we get the index of the button here
        if let index = self.subviews.firstIndex(of: button) {
            // and then call the buttonTappedAt with the index retrieved
            delegate?.buttonTappedAt(index: index)
        }
    }
}


And here is how the flow works, when a button inside the stack view is tapped :

What is Delegate? Understand it by building your own

Build and run the app, when you tap on the button, the delegate function will be executed, and you see the output on the console like this :

What is Delegate? Understand it by building your own

Table view delegate works similarly, when you tap a cell in table view,  table view will call the delegate (usually the view controller) 's didSelectRowAt function.

I hope you have a clearer picture on how delegate / dataSource works after reading this tutorial. Delegate isn't some magical pattern, it is just a variable referencing a ViewController or other object, then you just call the method on that variable.

You can download the completed sample delegate project here for double checking.

]]>
<![CDATA[How to transition from login screen to tab bar controller]]>

This article assumes your app will only have one scene all of the time. (ie. no multiple scenes at the same time in iPad).

At the end of this post, you will be able to implement a login screen (a navigation controller which can push to registration view), that transition

]]>
https://fluffy.es/how-to-transition-from-login-screen-to-tab-bar-controller/5eab29a62b012b0b9547f51aThu, 30 Apr 2020 19:43:34 GMTHow to transition from login screen to tab bar controller

This article assumes your app will only have one scene all of the time. (ie. no multiple scenes at the same time in iPad).

At the end of this post, you will be able to implement a login screen (a navigation controller which can push to registration view), that transition into the main tab controller like this :

How to transition from login screen to tab bar controller

Here are some common mistakes I saw other developers did (I did it too when first starting out) :

Common mistakes

1. Presenting the main view from login view on viewDidAppear every time

You might have attempted to present or push the main view controller on the login view controller when user has logged in previously, using code like this :

// LoginViewController.swift

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    // if user has logged in previously, present the main view controller
    
    if let loggedUsername = UserDefaults.standard.string(forKey: "username") {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let mainTabBarController = storyboard.instantiateViewController(identifier: "MainTabBarController")
        mainTabBarController.modalPresentationStyle = .fullScreen
        
        self.present(mainTabBarController, animated: true, completion: nil)
    }
}


This works but user will have to see the present/push animation every time they launch the app, even if they have already logged in previously :

How to transition from login screen to tab bar controller

(I used slow animation so it is easier to notice).

2. Keep presenting view controllers without dismissing

When user taps login, the main view controller is pushed/presented on top, then when user logs out, the login view controller is pushed / presented on top, and keeps repeating. This might look like a seamless experience, but it will keep the view controllers stacked without ever being released from memory, which can cause the app to run out of memory and crash eventually if user keeps on login and logout (unlikely but still).

If you never dismiss the view controllers, they will keep stacking on top like this :

How to transition from login screen to tab bar controller

And memory usage will keep increasing as more view controllers are added on top of the stack, notice the memory keeps increasing when I present a view controller :

How to transition from login screen to tab bar controller

Changing root view controller

Instead of pushing/ presenting view controller endlessly, a better way to do this is to change the root view controller directly. Root view controller is the bottom-most view controller on the stack. If you are using storyboard, the root view controller is the initial view controller you have set in your storyboard :

How to transition from login screen to tab bar controller

Instead of presenting the tab bar controller on top of login view controller:

How to transition from login screen to tab bar controller

We are going to switch the bottom-most view controller (root view controller) from the login view to main tab view like this :

How to transition from login screen to tab bar controller

Step 1: Show the correct view controller on app launch

If the user is logged in, we want to show the Main tab bar controller as the root view controller.

If the user is not logged in, we want to show the Navigation controller that contains the login and register view controller.

Assuming you have a storyboard with a flow similar to this, with navigation controller for the login/ register VC, and a tab bar controller once user has logged in  :

How to transition from login screen to tab bar controller

In order to access the view controllers in code, we need to assign them a storyboard identifier. Go ahead and assign storyboard ID for the navigation controller and tab bar controller :

How to transition from login screen to tab bar controller

(Storyboard Identifier: "LoginNavigationController" for the navigation controller)

How to transition from login screen to tab bar controller

(Storyboard Identifier: "MainTabBarController" for the tab bar controller)

After setting the storyboard identifier, head over to SceneDelegate.swift, and inside the willConnectTo session: function, add these lines :

// SceneDelegate.swift

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
    guard let _ = (scene as? UIWindowScene) else { return }
    
    // add these lines
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    
    // if user is logged in before
    if let loggedUsername = UserDefaults.standard.string(forKey: "username") {
        // instantiate the main tab bar controller and set it as root view controller
        // using the storyboard identifier we set earlier
        let mainTabBarController = storyboard.instantiateViewController(identifier: "MainTabBarController")
        window?.rootViewController = mainTabBarController
    } else {
        // if user isn't logged in
        // instantiate the navigation controller and set it as root view controller
        // using the storyboard identifier we set earlier
        let loginNavController = storyboard.instantiateViewController(identifier: "LoginNavigationController")
        window?.rootViewController = loginNavController
    }
}


Don't have a SceneDelegate.swift file in your project? Your project might be created using Xcode 10 or earlier, no worries, in this case, you can put the code in AppDelegate.swift, inside the didFinishLaunchingWithOptions method.

// AppDelegate.swift

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    // add these lines
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    
    // if user is logged in before
    if let loggedUsername = UserDefaults.standard.string(forKey: "username") {
        // instantiate the main tab bar controller and set it as root view controller
        // using the storyboard identifier we set earlier
        let mainTabBarController = storyboard.instantiateViewController(identifier: "MainTabBarController")
        window?.rootViewController = mainTabBarController
    } else {
        // if user isn't logged in
        // instantiate the navigation controller and set it as root view controller
        // using the storyboard identifier we set earlier
        let loginNavController = storyboard.instantiateViewController(identifier: "LoginNavigationController")
        window?.rootViewController = loginNavController
    }
  
    return true
}


Step 2 : Change the root view controller to main tab bar controller after login

So now instead of presenting the main tab bar controller, we will change the root view controller of the app directly after login.

Let's add a function to switch root view controller in the SceneDelegate.swift file, then we can use this function in all view controllers.

// SceneDelegate.swift

func changeRootViewController(_ vc: UIViewController, animated: Bool = true) {
    guard let window = self.window else {
        return
    }
    
    // change the root view controller to your specific view controller
    window.rootViewController = vc
}


Then in your login view controller, you can call this function after user successfully logs in :

// LoginViewController.swift
@IBAction func loginTapped(_ sender: UIButton) {
    // ...
    // after login is done, maybe put this in the login web service completion block

    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let mainTabBarController = storyboard.instantiateViewController(identifier: "MainTabBarController")
    
    // This is to get the SceneDelegate object from your view controller
    // then call the change root view controller function to change to main tab bar
    (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.changeRootViewController(mainTabBarController)
}


UIApplication.shared.connectedScenes.first is used to get the first scene connected to your app, this article assumes your app has only one scene (ie. user cannot open more than one scene of your app in iPad multitasking mode). As your app has only one scene, the .first will get the one and only scene of your app.

Then the delegate is an object (which usually is the SceneDelegate) conforming to the UISceneDelegate protocol, we can cast it back to SceneDelegate object using as? SceneDelegate , then we can access the changeRootViewController method

If your changeRootViewController() function is located in AppDelegate.swift, you can call it using :

(UIApplication.shared.delegate as? AppDelegate)?.changeRootViewController(mainTabBarController)


Now when user logs in, the root view controller will switch like this :

How to transition from login screen to tab bar controller

This transition feels abrupt, we can add some animation in the changeRootViewController to make the transition looks smoother :

// SceneDelegate.swift or AppDelegate.swift

func changeRootViewController(_ vc: UIViewController, animated: Bool = true) {
    guard let window = self.window else {
        return
    }

    window.rootViewController = vc

    // add animation
    UIView.transition(with: window,
                      duration: 0.5,
                      options: [.transitionFlipFromLeft],
                      animations: nil,
                      completion: nil)

}


You can choose from a list of preset transition :

How to transition from login screen to tab bar controller

For the code above, we will have a flip animation.

Step 3 :  Change the root view controller to login navigation controller after logout

Similar to previous step, we change the root view controller to the login navigation controller after user logs out.

// ProfileViewController.swift
@IBAction func logoutTapped(_ sender: UIButton) {
    // ...
		// after user has successfully logged out
  
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let loginNavController = storyboard.instantiateViewController(identifier: "LoginNavigationController")

    (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.changeRootViewController(loginNavController)
}


If your changeRootViewController() function is located in AppDelegate.swift, you can call it using :

(UIApplication.shared.delegate as? AppDelegate)?.changeRootViewController(mainTabBarController)


And now we have implemented the login / logout transition flow by changing root view controller! 🙌

How to transition from login screen to tab bar controller

]]>
<![CDATA[How to solve invalid_client error in Sign in with Apple]]>https://fluffy.es/how-to-solve-invalid_client-error-in-sign-in-with-apple/5e9b12bc2b012b0b9547f4f0Sat, 18 Apr 2020 14:50:06 GMTHow to solve invalid_client error in Sign in with Apple

One of the major roadblock on implementing Sign in with Apple is generating the client_secret parameter, which is required when sending a HTTP POST request to Apple's token validation endpoint (https://appleid.apple.com/auth/token), which exchange authorization code for an access token.

How to solve invalid_client error in Sign in with Apple

client_secret is a JWT (JSON Web Token) string you generate to prove that the HTTP request indeed comes from you (or your code), not originated from possible attacker.

Here's the usual suspect when invalid client error happens :

  1. Are you using the correct client_id in your HTTP request?
  2. Does your JWT header contains all the required parameters?
  3. Does your JWT payload contains all the required parameters, correctly?
  4. Is your JWT signature correct?

We will walk through each of these below and how to fix them. If you are confident that your JWT payloads and HTTP request are correct, you can jump to section 4 directly. We will be using this online JWT debugger (https://jwt.io/#debugger) to debug and verify JWT.

Are you using the correct client_id in your HTTP request?

If the authorization code comes from your iOS app, the client_id should be your iOS app bundle identifier.

How to solve invalid_client error in Sign in with Apple

If the authorization codes comes from your website / Android app (Apple redirect URI), the client_id should be your Services ID identifier.

How to solve invalid_client error in Sign in with Apple

Does your JWT header contains all the required parameters?

Paste in your JWT string into the "encoded" section of this JWT debugger (https://jwt.io/#debugger)

How to solve invalid_client error in Sign in with Apple

Your JWT header should contain "kid" and "alg" field.

The "kid" value should equal to your key ID, which is the .p8 key file generated in the Apple developer portal, with Sign in with Apple capability. If you don't have access to Apple developer portal, your .p8 key file should have the filename like "AuthKey_ABCDEF.p8",  the ABCDEF part is your Key ID.

How to solve invalid_client error in Sign in with Apple

The "alg" value should equal to "ES256", as Apple's server expect your JWT to be signed using Elliptive Curve Digital Signature Algorithm using P-256 and SHA-256.

Does your JWT payload contains all the required parameters, correctly?

Paste in your JWT string into the "encoded" section of this JWT debugger (https://jwt.io/#debugger)

How to solve invalid_client error in Sign in with Apple

Your JWT payload should only contain "iss", "iat", "exp", "aud" and "sub" field.

"iss" means issuer of this JWT, which is you or your company, this value should equal to your Team ID as shown in the Apple developer portal membership section :

How to solve invalid_client error in Sign in with Apple

"iat" means the time when this JWT was issued (created), this value should equal to the UNIX timestamp in seconds (not milliseconds) when your server generated the JWT. In Ruby, you can use Time.now.to_i to get this. If you are using Java, remember to convert this to seconds (instead of using milliseconds). This should be in number value, not enclosed in String, (ie: exp: 1587204602, instead of exp: "1587204602")

"exp" means the expiry time for this JWT, which the JWT will become invalid after this time. You should set a future time in UNIX timestamp in seconds (not milliseconds) for this field. The maximum acceptable value for this field is current time's timestamp + 15777000 seconds (6 months in the future) , usually I set it to 10 minutes from current time's timestamp ( eg: Time.now.to_i + 600 seconds ). This should be in number value, not enclosed in String, (ie: exp: 1587204602, instead of exp: "1587204602")

"aud" means the intended audience for this JWT, as we are sending this JWT to Apple's AppleID server, the value of this should always equal to "https://appleid.apple.com"

"sub" means the subject for this JWT. This should equal to the client_id value you used in the HTTP POST request. If you are using authorization code or access token gotten from iOS app, this field should equal to your iOS app bundle ID. If you are using authorization code or access token gotten from Apple's auth website redirect, this field should equal to your Services ID's identifier.

Don't have a clear picture on how to implement Sign in with Apple?

Get sample chapters of Practical Sign in with Apple.

Follow a complete step by step guide with code samples on implementing Sign in with Apple. (Ruby, PHP, Python and NodeJS)

Download Practical Sign in with Apple sample chapter (pdf)

Is your JWT signature correct?

Your JWT signature is the last part of the JWT string :

How to solve invalid_client error in Sign in with Apple

Here's a simplified diagram on how the signature is generated :

How to solve invalid_client error in Sign in with Apple

To check if the signature generated is correct, we can generate a public key from the .p8 private key, and decrypt the signature using the public key, and check if the decrypted SHA-256 hash matches the SHA-256 hash generated from hashing the header and payload.

Fortunately https://jwt.io/#debugger has built in signature verification function, we can just paste the public key in and it will verify it for us, so we only need to generate the public key.

To generate a public key from the .p8 private key, open Terminal app, and navigate (cd) to the directory containing your .p8 private key.

And run this command :

openssl ec -in AuthKey_123ABC456.p8 -pubout -out AuthKey_123ABC456_public.p8

Replace the "AuthKey_123ABC456.p8" with your private key file name, and replace "AuthKey_123ABC456_public" with the file name you want to use for the exported public key.

Running this command will generate a public key in the same folder. The public key file (.p8) file will look like this if you open it with text editor :

-----BEGIN PUBLIC KEY-----
ABCDEFGHIJKLMNOP......
-----END PUBLIC KEY-----

Go to JWT.io debugger, select ‘ES256’  as the algorithm and paste your client secret JWT string, then at the bottom right section, paste the public key text (including the “—BEGIN PUBLIC KEY—-“ and “—END PUBLIC KEY—” lines) into the "verify signature" -> public key box.

How to solve invalid_client error in Sign in with Apple

You should see a  “Signature Verified” status at the bottom left if the JWT is signed correctly :

How to solve invalid_client error in Sign in with Apple

If you are seeing “Invalid Signature”, it means that you are  either using the wrong private key to sign it, or there’s something wrong with the signing step.

Make sure the JWT library you used supports ES256 elliptic curve encryption.

If your own code or the library you used is using openssl_sign function to generate the signature for ES256, it will generate a signature which uses a DER-encoded ASN.1 structure (with size > 64).

The correct digital signature is the concatenation of two unsigned integers, denoted as R and S, which are the result of the Elliptic Curve (EC) algorithm. The length of R || S is 64.

The way to fix this is to convert the DER-encoded signature into a raw concatenation of the R and S values, as explained further in this StackOverflow answer.

…..or you can use the libraries I listed below for generating / signing JWT, I have tested each of them myself and they produce the correct signature for ES256.

  1. Python - [https://github.com/mpdavis/python-jose]
  2. Node.js - [https://github.com/panva/jose]
  3. PHP - [https://github.com/firebase/php-jwt]
  4. Ruby - [https://github.com/jwt/ruby-jwt]

This article is an excerpt from the book Practical Sign in with Apple , if you want a complete step by step guide on implementing Sign in with Apple (from iOS client side to backend communication with Apple to adding SIWA support on your website), with code samples which you can plug in directly (Ruby, PHP, Python and NodeJS), give the sample chapters a try!

Download Practical Sign in with Apple sample chapter (pdf)

]]>
<![CDATA[Open app in specific view when push notification is tapped (iOS 13+)]]>

Say you have an app and want to redirect user to a specific view when a push notification is tapped, eg: going to a specific chat room in Telegram after tapping push notification of that message. How do we proceed to implement this? 🤔

This tutorial assumes your app have only

]]>
https://fluffy.es/open-app-in-specific-view-when-push-notification-is-tapped-ios-13/5e9840442b012b0b9547f4c5Thu, 16 Apr 2020 11:29:34 GMTOpen app in specific view when push notification is tapped (iOS 13+)

Say you have an app and want to redirect user to a specific view when a push notification is tapped, eg: going to a specific chat room in Telegram after tapping push notification of that message. How do we proceed to implement this? 🤔

This tutorial assumes your app have only one scene.

If you app project file doesn't have SceneDelegate.swift file (created on Xcode 10 or earlier), check this tutorial instead.

Just want the code already? Click here

At the end of this tutorial, you will be able to implement this :

Open app in specific view when push notification is tapped (iOS 13+)

(Notice that after tapping the push notification, the app moves to the conversation with Sans).

In the previous article on performing action when user tap on notification, we know that to perform an action when user tap on notification, we need to use the didReceive response: method from UNUserNotificationCenterDelegate.

// AppDelegate.swift

// remember to set delegate for notification center 
// UNUserNotificationCenter.current().delegate = self

extension AppDelegate: UNUserNotificationCenterDelegate{
    
  // This function will be called right after user tap on the notification
  func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
      
    // tell the app that we have finished processing the user’s action / response
    completionHandler()
  }
}

Now that we can process the notification tap, how do we access the view controllers from AppDelegate?

Scene and Window

Since iOS 13, Apple has introduced a new concept called 'Scene' , as in you can open more than one scene of the same app.

Open app in specific view when push notification is tapped (iOS 13+)

And when you create a new iOS app project using Xcode 11 and above, Xcode will create a SceneDelegate.swift for you. Prior to iOS 13, the top of the view hierachy is the UIWindow, and we can access the root view controller of a window by using window.rootViewController in the AppDelegate.

But since iOS 13, the top  of the view hierachy is not UIWindow anymore, it's UIScene , and the UIWindow is one level below it.

Open app in specific view when push notification is tapped (iOS 13+)

The window variable is not available anymore in AppDelegate, which is moved to SceneDelegate instead.

Open app in specific view when push notification is tapped (iOS 13+)

But the function that will be called when push notification is tapped is still in App Delegate, because push notification tap is app-wide, which is not limited to just one scene, how can we change the view controllers being shown inside the App Delegate code?

The main goal is to access the window.rootViewController from AppDelegate so we can change the structure of current view controllers presented to user.

This is how we can access the root view controller from the first scene (assuming your app has only one scene, if your app has multiple scene, you need to write code to decide which scene to use).

(UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController 

UIApplication.shared.connectedScenes is a set (unordered array) that represents the current scenes of the app shown on the iOS device.

We then get the first scene using .first , and get its delegate (UISceneDelegate protocol type).

In order to access the window property as defined in SceneDelegate.swift file, we need to cast it to SceneDelegate using as? SceneDelegate .

Lastly, we access the rootViewController through the window property, viola!

You can then change the root view controller in the AppDelegate.swift like this :

// when user tap on the push notification
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        
    guard var rootViewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController else {
        return
    }

    // change your view controller here
    rootViewController = UIViewController()
}

Example with tab bar controllers and navigation controllers

Say your app flow is like this, a tab bar controller as the initial view controller and each tab is embed inside navigation controller :

Open app in specific view when push notification is tapped (iOS 13+)

The root view controller is our entry point to access view controllers from AppDelegate. One naive approach would be changing the root view controller to the view controller we want to show, when the user tap on the push notification.

Say we want to show the ConversationViewController when user tap on push notification :

Open app in specific view when push notification is tapped (iOS 13+)

We can change the root view controller like this :

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
      
		// the root view controller
		guard var rootViewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController else {
        return
    }
    
    let storyboard = UIStoryboard(name: "Main", bundle: nil)

    // instantiate the view controller from storyboard
    if  let conversationVC = storyboard.instantiateViewController(withIdentifier: "ConversationViewController") as? ConversationViewController {

        // set the view controller as root
        rootViewController = conversationVC
    }
    
    // tell the app that we have finished processing the user’s action / response
    completionHandler()
}


Open app in specific view when push notification is tapped (iOS 13+)

The problem with this approach is that by replacing the root view controller with the new view controller, we have removed the whole current stack including tab bar controller and also the navigation controller, making the user unable to go back to the previous view controller! 😱

A better solution would to be pushing the new view controller into the current navigation controller (assuming every tab has its own navigation controller).

Pushing new view controller into the current navigation controller

A better approach would be pushing the new view controller to the existing navigation controller.

This section assume that your app uses a navigation controller to move around view controllers. All tabs in the tab bar controller contain a navigation controller.

Open app in specific view when push notification is tapped (iOS 13+)

As the tab bar controller is the root view controller, we can traverse to the navigation controller, and push the new view controller into it like this :

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
    
    // retrieve the root view controller (which is a tab bar controller)
    guard let rootViewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController else {
        return
    }
  
    let storyboard = UIStoryboard(name: "Main", bundle: nil)

    // instantiate the view controller we want to show from storyboard
    // root view controller is tab bar controller
    // the selected tab is a navigation controller
    // then we push the new view controller to it
    if  let conversationVC = storyboard.instantiateViewController(withIdentifier: "ConversationViewController") as? ConversationViewController,
        let tabBarController = rootViewController as? UITabBarController,
        let navController = tabBarController.selectedViewController as? UINavigationController {

            // we can modify variable of the new view controller using notification data
            // (eg: title of notification)
            conversationVC.senderDisplayName = response.notification.request.content.title
            // you can access custom data of the push notification by using userInfo property
            // response.notification.request.content.userInfo
            navController.pushViewController(conversationVC, animated: true)
    }
    
    // tell the app that we have finished processing the user’s action / response
    completionHandler()
}

This code will produce the following result :

Open app in specific view when push notification is tapped (iOS 13+)

Want to try out the sample push notification app project?

Get sample Xcode project containing code of showing specific view controller when push notification is tapped, try it out!

https://github.com/fluffyes/tapPushOpenView/archive/master.zip

Presenting view controller modally

What if the current showing view controller of your app isn't contained inside a navigation controller? You can choose to present it modally (remember to check if there is any other view controller is being presented before presenting it), but you would have to traverse from the root view controller to the current showing view controller manually in AppDelegate, and then call .present() on the current view controller.

if  let conversationVC = storyboard.instantiateViewController(withIdentifier: "ConversationViewController") as? ConversationViewController,
    let tabBarVC = rootViewController as? UITabBarController {
    
    tabBarVC.selectedViewController.present(conversationVC, animated: true, completion: nil)
}
]]>
<![CDATA[Inspect app folder in simulators and real device]]>

Your app might store some data or files in the Documents folder, and sometimes you might want to check if the data / files are stored correctly by inspecting them. How do we find the app folder containing the data?

You can also inspect the UserDefaults data by going to AppFolder/

]]>
https://fluffy.es/inspect-app-folder-in-simulators-and-real-device/5e85fb172b012b0b9547f4a2Thu, 02 Apr 2020 14:56:02 GMTInspect app folder in simulators and real device

Your app might store some data or files in the Documents folder, and sometimes you might want to check if the data / files are stored correctly by inspecting them. How do we find the app folder containing the data?

You can also inspect the UserDefaults data by going to AppFolder/Library/Preferences/Your_app_bundle_id.plist .

Simulators

For simulator, the app folder is stored in your Mac. You can get the path of this folder by printing  NSHomeDirectory()

print("app folder path is \(NSHomeDirectory())")

You can put this line in viewDidLoad function, and get the path in the Xcode console.

Inspect app folder in simulators and real device

Copy the path, open Finder, then select "Go" -> "Go to Folder" (shortcut key Command + Shift + G), then paste in the path and press Enter.

Inspect app folder in simulators and real device

Inspect app folder in simulators and real device

Inspect app folder in simulators and real device

If you don't want to modify your code, there's another way to locate the app folder.

Launch the simulator, then open Terminal app, type in the code below, replace the bundle ID with your app bundle ID :

open `xcrun simctl get_app_container booted BUNDLE_ID_OF_THE_APP data`

This will open the folder automatically.

Real device

For real device, you will need to download the whole app container to your Mac to inspect them.

Open Xcode, Select  Window > Devices and Simulators

Inspect app folder in simulators and real device

Select your device, then choose the app you want to inspect, then select "Download container" to download the app folder to your Mac.

Inspect app folder in simulators and real device

The app container will be in .xcappdata format, you can inspect it by right clicking it -> "Show package contents"

Inspect app folder in simulators and real device

Inspect app folder in simulators and real device

]]>
<![CDATA[Introduction to Localization (add additional language support to your app)]]>

Localization means making your app support additional languages. Usually we will start with english as the base language as default, then slowly adding more language support on top of it.

translation demo

Table of Contents :

  1. Adding new language
  2. Adding and using translated text
  3. Localizing word with parameters
  4. Preferred language and how to
]]>
https://fluffy.es/introduction-to-localization/5e64d81b2b012b0b9547f451Sun, 08 Mar 2020 11:40:41 GMTIntroduction to Localization (add additional language support to your app)

Localization means making your app support additional languages. Usually we will start with english as the base language as default, then slowly adding more language support on top of it.

Introduction to Localization (add additional language support to your app)

Table of Contents :

  1. Adding new language
  2. Adding and using translated text
  3. Localizing word with parameters
  4. Preferred language and how to change them
  5. Detecting the current app language
  6. Localizing App Name and system's message
  7. Direct user to change language

Before starting localization of your app, make sure the project has "Use Base Internationalization" checked. This is checked by default when you start a new project in Xcode 11.

Introduction to Localization (add additional language support to your app)

What does "Use Base Internationalization" mean?

When you create a new iOS project in Xcode 11, Xcode will auto generate the resources for the default language (which is the "base" language), like Main.storyboard and LaunchScreen.storyboard, and place them into the "Base.lproj" folder. The "base" means other language added later on will use this as base to refer to.

Introduction to Localization (add additional language support to your app)

Adding new language

Now that we have base localization set, we can proceed to add more language by clicking the "+" button below :

Introduction to Localization (add additional language support to your app)

After selecting a language, Xcode will ask if you want to create localization for current resource (storyboard / XIBs) :

Introduction to Localization (add additional language support to your app)

After you click 'Finish' , Xcode will create a Strings file for each of the resources.

For example, say your Main.storyboard has a view controller with two labels like this :

Introduction to Localization (add additional language support to your app)

If you have included Main.storyboard (checked in the dialog above) and click 'Finish', Xcode will create a 'Main.strings' file (spanish) for translating the labels shown in the storyboard.

Introduction to Localization (add additional language support to your app)

You can change the text of "Good morning" to "Buenos días" in Main.strings, then when you run the app in a device which primary language is set to Spanish, it will show "Buenos días".

I prefer to create a separate Strings file which contain all the text that need to be localized, instead of separating the text based on storyboard files. In this tutorial we will ignore the auto-generated Main.strings file, and creating our own Strings file.

Right click your project folder, select 'New File' , then scroll down to find 'Strings', select it and click 'Next'.

Then name the file as 'Localizable.strings' and click 'Create'. Older Xcode versions will automatically create this file for you when you localize the project, recent Xcode version doesn't do this automatically anymore, hence we need to create it manually.

Introduction to Localization (add additional language support to your app)

Introduction to Localization (add additional language support to your app)

Introduction to Localization (add additional language support to your app)

Now you have a 'Localizable.strings' created for the base language. We can then add a Spanish version (and other languages) of this file, by clicking the 'Localize' button on the file :

Introduction to Localization (add additional language support to your app)

After clicking 'Localize', Xcode will ask if you want to localize the file, and select a default language for it. Usually I use 'English' as the default language and gradually add more language support to it.

Introduction to Localization (add additional language support to your app)

Now on the 'Localization' section of the File inspector, we can see the language available for this project (You can add more language by going back to the project settings and click '+'). Check the 'Spanish' (or the language you have added) to add localization support.

Introduction to Localization (add additional language support to your app)

After checking the additional language, you can expand the "Localizable.strings" and see there's multiple language version :

Introduction to Localization (add additional language support to your app)

When localizing a file, Xcode will create separate language folder for that file. Eg: "es.lproj" for spanish ("es"), and "en.lproj" for english, etc. The localized file is then put into these folders.

Introduction to Localization (add additional language support to your app)

Introduction to Localization (add additional language support to your app)

Adding and using translated text

To add translation text, we need to input key value pair of each translation in this format:

"key" = "value";

For example, the english Localizable.strings file looks like this

Introduction to Localization (add additional language support to your app)

Then in the spanish Localizable.strings file :

Introduction to Localization (add additional language support to your app)

Notice that we uses the same key for the same text translation in both file, and each translation is separated by a semicolon (;).

To use the translation text, we can call Bundle.main.localizedString(forKey:, value: , table: ) :

class ViewController: UIViewController {
    
    @IBOutlet weak var helloLabel: UILabel!
    @IBOutlet weak var goodMorningLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        // return 'Hello' for english, 'Hola' for spanish
        helloLabel.text = Bundle.main.localizedString(forKey: "Hello", value: "Hello", table: "Localizable")
        
        // return 'Good Morning' for english, 'Buenos días' for spanish
        goodMorningLabel.text = Bundle.main.localizedString(forKey: "Good Morning", value: "Good Morning", table: "Localizable")
    }
}


The Bundle.main.localizedString function will search for the value of the specified key in forKey parameter, and return it if it exist. If no such key exist in the strings file, the function will return the string passed into the value parameter.

The table parameter specifies the filename of the strings file to search, in the example above, it will search the key/value pair in "Localizable.strings" file. If you pass in nil for the table parameter, it will look for "Localizable.strings" by default.

Now if you run the app in spanish language, you will see the following output :

Introduction to Localization (add additional language support to your app)

You might have seen NSLocalizedString() function before, it's a shortcut function Apple has provided us. NSLocalizedString take in two parameters, the key and comment, and then it will call Bundle.main.localizedString() by passing the key to forKey and value parameters, and set the table to nil , which will search for "Localizable.strings" file by default.

Introduction to Localization (add additional language support to your app)

The comment parameter is for your own reference, it doesn't do anything, you can think of it like the comment you place in your code.

We can then replace the Bundle.main.localizedString with the shorter NSLocalizedString like this :

 override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    helloLabel.text = NSLocalizedString("Hello", comment: "")
    goodMorningLabel.text = NSLocalizedString("Good Morning", comment: "")
}

Localizing word with parameters

Sometimes we might want to localize text with some parameter, eg: "Hello (username)" , which the username is an input from the user.  To interpolate the parameter, we can use the String format function String(format: , args). According to Apple's string programming guide, we can use "%@" to interpolate string, "%d" for integer etc.

We can use "%@" inside the translated text as placeholder, then replace it with the actual username using String(format:, args). Here's an illustration on how it works :

Introduction to Localization (add additional language support to your app)

You can use as many parameters as you like,  like this : String(format: NSLocalizedString("Hello first middle last name", comment: ""), "First name", "Middle name", "Last name"). As the String(format:, args:) accepts multiple parameters for args.

Preferred language and how to change them

When user launch your app for the first time, iOS will decide which language to use based on their preferred language order, not the iPhone language.

Introduction to Localization (add additional language support to your app)

Your user might set iPhone language to English, but using another language as the most preferred language (the top row of preferred language order).

iOS will choose the first matching language from top to bottom in the preferred language list to launch your app. If your app doesn't support all the languages listed in the preferred languages , iOS will try to make guesses to choose the nearest language possible. (eg: your app support Portuguese (Portugal) but the user only have Portuguese(Brazil) and English, then iOS will choose Portuguese (Portugal)).

Source: https://developer.apple.com/videos/play/wwdc2016/201/ , timestamp: 11:02

App-specific preferred language is introduced in iOS 13, this means that user can change preferred language for that specific app. In the Settings app, user can scroll to your app, and change the preferred language for just your app without changing the device preferred languages.

Introduction to Localization (add additional language support to your app)

After user change the preferred language for your app and switch back to your app, your app will be relaunched and NSLocalizedString will use the updated language.

Introduction to Localization (add additional language support to your app)

It can be quite troublesome to go to the Settings app, and change language just to test your localization every time you update a text.

To shortcut this process, we can set the app to always launch in a language during debug session. To do this, in Xcode, select Product > Scheme > Manage Schemes ...

Introduction to Localization (add additional language support to your app)

In the scheme window , double click your app scheme :

Introduction to Localization (add additional language support to your app)

Then in the Run/debug tab, change the "Application Language" to your desired language.

Introduction to Localization (add additional language support to your app)

And now every time you build and run your app from Xcode, it will use that language as the preferred language.

Detecting the current app language

Say you want to show / hide some UI based on the current app language (not the device language), you can get the current selected app language with Bundle.main.preferredLocalizations.first .

// if the app language is japanese
if(Bundle.main.preferredLocalizations.first == "ja"){
    print("the app is using japanese language")
}


From Apple's documentation, preferredLocalizations will return an array of strings,  containing language IDs for localizations in the bundle. The strings are ordered according to the user's language preferences and available localizations. We use the .first to get the most preferred language of the app (which the app is using).

You might see some StackOverflow answer suggesting to use Locale.preferredLanguage.first for getting the current language, keep in mind that this will get the first preferred language of the device, not the app.

Introduction to Localization (add additional language support to your app)

Localizing App Name and system's message

To localize app name and other text that are defined in the Info.plist file (eg: Camera usage description), we will need to create a InfoPlist.strings file. Right click your project folder, select 'New File...' > 'Strings File' , and name it as InfoPlist.strings.

Introduction to Localization (add additional language support to your app)

Introduction to Localization (add additional language support to your app)

Introduction to Localization (add additional language support to your app)

Localize it by clicking the 'Localize' button,

Introduction to Localization (add additional language support to your app)

Then check all the language you want to localize in the "Localization" section.

For app name, the key is "CFBundleDisplayName".

Introduction to Localization (add additional language support to your app)

For the other system permission messages like camera usage description, you can get the key of it from Info.plist.

Normally when you open Info.plist, Xcode will show it as a list like this :

Introduction to Localization (add additional language support to your app)

To view the raw key value, we can right click "Info.plist", select "Open As" > "Source Code" :

Introduction to Localization (add additional language support to your app)

Then you can find the key for the Camera Usage Description, which is enclosed between <key> and </key> tag :

Introduction to Localization (add additional language support to your app)

Then you can use this key for the translation text in InfoPlist.strings file.

Note that for app name and Info.plist related text localization, it will use the device preferred language, instead of the app's preferred langauge.

Introduction to Localization (add additional language support to your app)

Introduction to Localization (add additional language support to your app)

Direct user to change language

Since iOS 13, Apple recommend us to direct user to the Settings app to change language, instead of changing the language manually in the app (eg: using Hackish way like changing the AppleLanguages UserDefault key, or use some method swizzling to change the bundle), as there is a lot of work under the hood done by the OS.

Introduction to Localization (add additional language support to your app)

Source: Creating Great Localized Experience with Xcode 11 WWDC video, timestamp: 3:01 .

You can have a change language button in your app, and when tapped, open the Settings app > your app, by using UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) .

]]>
<![CDATA[How to expand and contract height of a UITableView cell when tapped]]>

To avoid confusion, this article focuses on expanding and contracing the height of a single tableview cell when it is tapped. I googled around and found out many people used the same terms for inserting new cells under the tapped cell, which is not what this article is about, I

]]>
https://fluffy.es/how-to-expand-tableview-cell/5e4040cc2b012b0b9547f41aSun, 09 Feb 2020 17:32:13 GMTHow to expand and contract height of a UITableView cell when tapped

To avoid confusion, this article focuses on expanding and contracing the height of a single tableview cell when it is tapped. I googled around and found out many people used the same terms for inserting new cells under the tapped cell, which is not what this article is about, I will write on this topic in the future.

At the end of this tutorial, you will be able to implement this :

How to expand and contract height of a UITableView cell when tapped

This tutorial assumes you know how to implement tableview cell with dynamic height (Automatic Dimension), as in each cell can contain different content height, eg: lengths of label text, different image sizes.

Say we have a table view cell with image and label set like below :

How to expand and contract height of a UITableView cell when tapped

You can download the starter project here : Expanding cell starter project

The image view (avatarImageView) and label (messageLabel) are initialized in the cellForRowAt function :

// ViewController.swift

extension VariableViewController : UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 3
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      
      // if cant dequeue cell, return a default cell
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? VariableTableViewCell else {
            print("failed to get cell")
            return UITableViewCell()
        }
        
        cell.avatarImageView.image = UIImage(named: avatars[indexPath.row])
        cell.messageLabel.text = messages[indexPath.row]
        cell.messageLabel.numberOfLines = 0
      
        return cell
    }
}


Before we implement the expanding / contracting animation, we would need to declare an IndexSet to keep track of which cell have been expanded, by storing the row index into the IndexSet, so that when we tap on the expanded cell, it would contract instead of expanding.

Use IndexSet to keep track of which cell has been expanded

From Apple documentation on IndexSet,

A collection of unique integer values that represent the indexes of elements in another collection.

You can think of IndexSet like an array, it can hold multiple integers, but non of the integers are repeating. This way, we won't need to worry about adding repeating row index into the array.

How to expand and contract height of a UITableView cell when tapped

"Why can't we just use an array of boolean to track which cell has been expanded?" , I heard you, this solution works too, say you can check if a cell has been expanded  like this :

// 10 cells
var expandedCells : [Boolean] = Array(repeating: false, count: 10)

if(expandedCells[index] == true)
{
  // ... show the cell expanded
}


This works well if you have a fixed amount of cells in the table view, but if you have dynamic data that will be added from external API, say like Facebook timeline which keep adding more statuses as you scroll, you will need to adjust the size of the expandedCells array as more cells are being added, this process can easily be error prone!

By using IndexSet, we won't need to worry about the size for the dataSource, we just need to add or remove index of the cell that is expanded.

We can define an IndexSet to keep track of the index of cell expanded in the view controller like this :

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    
    // this will contain the index of the row (integer) that is expanded
    var expandedIndexSet : IndexSet = []
  
    // ...
}


Animating the cell to expand and contract

Now that we have an IndexSet to keep track of which cell has been expanded, we can layout the cell expanded / contracted state in cellForRowAt like this :

extension VariableViewController : UITableViewDataSource {
  // ...

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? VariableTableViewCell else {
      print("failed to get cell")
      return UITableViewCell()
    }

    cell.avatarImageView.image = UIImage(named: avatars[indexPath.row])
    cell.messageLabel.text = messages[indexPath.row]

    // if the cell is expanded
    if expandedIndexSet.contains(indexPath.row) {
      // the label can take as many lines it need to display all text
      cell.messageLabel.numberOfLines = 0
    } else {
      // if the cell is contracted
      // only show first 3 lines
      cell.messageLabel.numberOfLines = 3
    }

    return cell
  }
}


As we want the cell to be expanded / contracted on tap, we then implement the didSelectAtRow delegate function :

extension ViewController : UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        
        // if the cell is already expanded, remove it from the indexset to contract it
        if(expandedIndexSet.contains(indexPath.row)){
            expandedIndexSet.remove(indexPath.row)
        } else {
            // if the cell is not expanded, add it to the indexset to expand it
            expandedIndexSet.insert(indexPath.row)
        }
        
        // the animation magic happens here
        // this will call cellForRow for the indexPath you supplied, and animate the changes
        tableView.reloadRows(at: [indexPath], with: .automatic)
    }
}


The key for the animation is calling tableView.reloadRows(at:, with:) . You supply an array of rows (indexpath) that needs to be reloaded, then the tableview will reload them by calling cellForRowAt: on these cell to layout them again, and animate the transition with the RowAnimation you passed into the with: parameter.

How to expand and contract height of a UITableView cell when tapped

Here's a list of available RowAnimation we can use to animate the cell row changes, but mostly we will use the .automatic or .fade effect as it looks better.

Implementing the cell animation was easier than I originally thought! Thanks reloadRows for the heavy lifting.

If you want to check if an UILabel is truncated (with "..." at the end of text because of clipping), I have written a short extension here, this might be useful if you are checking whether to show a "read more" button on the tableview cell depending if the label is being truncated. eg. If the the label text is short and didn't get truncated, don't show the "read more" button.

Download the sample expanding cell project

Not sure if you are doing it right on the expanding cell?

Download the sample project and compare it yourself!

https://github.com/fluffyes/expandingCell

How to expand and contract height of a UITableView cell when tapped

]]>
<![CDATA[How to implement dynamic height table view cell (self sizing)]]>https://fluffy.es/dynamic-height-tableview-cell/5e2978a82b012b0b9547f3f5Thu, 23 Jan 2020 10:49:04 GMTHow to implement dynamic height table view cell (self sizing)

It's common to implement table view which contain cells with different length of text, as the content are retrieved from web APIs.

When implementing a dynamic height tableview cell, one common issue we get is that the text either got clipped or two elements get overlapped :

How to implement dynamic height table view cell (self sizing)

A quick search on google on 'how to make a dynamic height table view cell' will tell you to put in this two line :

tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 80

Putting these two lines seems easy, but somehow the cell refuse to expand to the right height even when the label exceed one line, and before you know it you have already spent two hours wrangling with the constraints inside the cell and still not working.

The key to making this work is actually setting enough constraints for the elements inside the cell so that Auto layout knows how to calculate the height of the cell.

To let Auto Layout know how to calculate the height of the cell, we need to create sufficient constraints from top of the cell's content view to the bottom.

How to implement dynamic height table view cell (self sizing)

For the cell above with two labels, we need to define the vertical spacing constraint from author name label to the cell content view top (A), the vertical spacing between author name label and quote label (C) and bottom constraint from quote label to the cell content view bottom (E).

A :

How to implement dynamic height table view cell (self sizing)
How to implement dynamic height table view cell (self sizing)

C :

How to implement dynamic height table view cell (self sizing)
How to implement dynamic height table view cell (self sizing)

E :

How to implement dynamic height table view cell (self sizing)
How to implement dynamic height table view cell (self sizing)

As for the height of author name label (B) and height of quote label (D), we do not need to set a height constraint for them as UILabel has intrinsic content size. UILabel has intrinsic content size, this means that its width and height can be derived from the text string of the label and the font type and font size used.

As long as Auto Layout is able to calculate the height of the cell from top to bottom, it will show dynamic height cell nicely.

The overlapping example at the start of this article happens because I didn't define the vertical spacing constraint between author name label and quote label (C) :

How to implement dynamic height table view cell (self sizing)
How to implement dynamic height table view cell (self sizing)

This makes Auto Layout unable to calculate the height of the cell.

When Auto Layout is unable to calculate the height of the cell due to insufficient constraint, it will use the default row height or the row height you have defined in Interface Builder, or the height derived from heightForRowAt function.

If we add another image view in the middle, then here's the vertical constraints we need to set (colored in Red, A, C, D, E and G) :

How to implement dynamic height table view cell (self sizing)

Image view without an image selected has no intrinsic content size, hence we need to set an explicit height constraint for it.

The key to achieve dynamic height table view cell is to let Auto Layout know how to calculate the height of the cell.

This article is an excerpt from the book Making Sense of Auto Layout , if you feel like you have no idea what you are doing when adding / removing / editing constraints,  give the sample chapter a try!

Download the sample dynamic cell height project

Not sure if you are doing it right on the self sizing cell height?

Download the sample project and compare it yourself!

https://github.com/fluffyes/dynamicHeightTable

How to implement dynamic height table view cell (self sizing)

]]>
<![CDATA[Move view when keyboard is shown (guide)]]>

Just want the code to move the view up?

Just want the code to scroll the scrollview up?

Most likely you have worked on an app with multiple textfields, and when the keyboard show up, there's a chance that your text field will get covered by the keyboard! One solution

]]>
https://fluffy.es/move-view-when-keyboard-is-shown/5e1a04352b012b0b9547f3b2Sat, 11 Jan 2020 17:46:44 GMTMove view when keyboard is shown (guide)

Just want the code to move the view up?

Just want the code to scroll the scrollview up?

Most likely you have worked on an app with multiple textfields, and when the keyboard show up, there's a chance that your text field will get covered by the keyboard! One solution for this is to move the view up when a keyboard is presented.

Move view when keyboard is shown (guide)

Right before a keyboard is shown on the screen, iOS will send a notification named UIResponder.keyboardWillShowNotification to your current active UIResponder (can be your view or view controller). The notification contains information of the keyboard such as the size of they keyboard, which you can use to calculate how much distance should the view move upward.

We can get the frame of the keyboard by accessing the key UIResponder.keyboardFrameEndUserInfoKey from the userInfo dictionary of the notification. I noticed some of the StackOverflow answers use the key keyboardFrameBeginUserInfoKey instead of keyboardFrameEndUserInfoKey , which might produce incorrect output as keyboardFrameBeginUserInfoKey contains the frame info of the keyboard before the keyboard animation starts (animation of keyboard move up from bottom of the screen).

Apple documentation recommends using keyboardFrameEndUserInfoKey as this contain the frame of the keyboard after the animation has ended.

// the frame of the keyboard
let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue


We can then use this keyboardSize's height to move the root view Y origin upward like this :

// ViewController.swift

// in ViewDidLoad, observe the keyboardWillShow notification
override func viewDidLoad() {
    super.viewDidLoad()
  
    // call the 'keyboardWillShow' function when the view controller receive notification that keyboard is going to be shown
    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
}

@objc func keyboardWillShow(notification: NSNotification) {       
    guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {
       // if keyboard size is not available for some reason, dont do anything
       return
    }
  
  // move the root view up by the distance of keyboard height
  self.view.frame.origin.y = 0 - keyboardSize.height
}


And when user dismiss the keyboard, another notification will be fired, UIResponder.keyboardWillHideNotification. We then observe for this notification and move back the root view frame origin to 0 (it's original origin is zero).

// ViewController.swift

// in ViewDidLoad, observe the keyboardWillShow notification
override func viewDidLoad() {
    super.viewDidLoad()
  
    // call the 'keyboardWillShow' function when the view controller receive the notification that a keyboard is going to be shown
    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
  
      // call the 'keyboardWillHide' function when the view controlelr receive notification that keyboard is going to be hidden
    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}

@objc func keyboardWillShow(notification: NSNotification) {
        
    guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {
       // if keyboard size is not available for some reason, dont do anything
       return
    }
  
  // move the root view up by the distance of keyboard height
  self.view.frame.origin.y = 0 - keyboardSize.height
}

@objc func keyboardWillHide(notification: NSNotification) {
  // move back the root view origin to zero
  self.view.frame.origin.y = 0
}


This will produce the output like below :

Move view when keyboard is shown (guide)

If you just want to move up the view origin y when keyboard is shown, this should do the trick. However this simple approach might push some textfield too high of the screen :

Move view when keyboard is shown (guide)

In this scenario, the username field is too high far off the screen and user can't view what they are typing! 😅

One of the solution for this scenario is to check if the text field will be covered by the keyboard when the keyboard appear, if it will be covered , then move the whole view up; if not, then just leave the view as it is and show the keyboard since the text field is still visible.

Move view when keyboard is shown (guide)

How can we check if a textfield will get covered by the keyboard? First we get the bottomY value of the textfield (the bottom edge of the textfield), and compare it to the visible range (the screen area that is still visible after keyboard is presented). If the bottomY value is larger than the visible range (ie. the bottomY value of textfield is larger than the bottomY value of visible range), then only we move the view up.

Move view when keyboard is shown (guide)

As there are multiple textfields on the screen, how do we get the bottomY of the current selected textfield by the user?

We can declare a variable to keep track of the current active textfield, say activeTextField, and assign the selected textfield to this variable when user select a textfield.

We set the textfield delegate to self (to the view controller), and we listen to when user select a textfield in the UITextFieldDelegate function, textFieldDidBeginEditing.

// ViewController.swift
class ViewController: UIViewController {

  @IBOutlet weak var usernameTextField: UITextField!

  @IBOutlet weak var passwordTextField: UITextField!

  // ... other text field

  // to store the current active textfield
  var activeTextField : UITextField? = nil
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // ....
    
    // add delegate to all textfields to self (this view controller)
    usernameTextField.delegate = self
    passwordTextField.delegate = self
    emailTextField.delegate = self
  }
}

extension ViewController : UITextFieldDelegate {
  // when user select a textfield, this method will be called
  func textFieldDidBeginEditing(_ textField: UITextField) {
    // set the activeTextField to the selected textfield
    self.activeTextField = textField
  }
	
  // when user click 'done' or dismiss the keyboard
  func textFieldDidEndEditing(_ textField: UITextField) {
    self.activeTextField = nil
  }
}


And then we compare the activeTextField's maxY (bottomY) value to the visibleRange in the keyboardWillShow function :

@objc func keyboardWillShow(notification: NSNotification) {

  guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {

    // if keyboard size is not available for some reason, dont do anything
    return
  }

  var shouldMoveViewUp = false

  // if active text field is not nil
  if let activeTextField = activeTextField {

    let bottomOfTextField = activeTextField.convert(activeTextField.bounds, to: self.view).maxY;
    
    let topOfKeyboard = self.view.frame.height - keyboardSize.height

    // if the bottom of Textfield is below the top of keyboard, move up
    if bottomOfTextField > topOfKeyboard {
      shouldMoveViewUp = true
    }
  }

  if(shouldMoveViewUp) {
    self.view.frame.origin.y = 0 - keyboardSize.height
  }
}


Build and run your app, you should see the view only move up if the text field selected is located near the keyboard :

Move view when keyboard is shown (guide)

If your view have more than 3 textfields, I recommend putting them inside a scrollview (You can check out how to place UI inside scrollview in Storyboard here) and use some code to scroll up the scrollview when a keyboard is shown, we will explain more about this in the next section.

Download the sample keyboard move up project. (normal + scrollview)

Not sure if you are doing it right on handling the view when keyboard shows?

Download the sample project and compare it yourself!

https://github.com/fluffyes/keyboardmoveup/archive/master.zip

Scrolling up scrollview when keyboard is shown

Assuming you have set up a scrollview with textfields inside it like this :

Move view when keyboard is shown (guide)

We can add bottom padding into the scrollview using its contentInset property, so the content inside will be moved up when a keyboard is shown.

You can think of contentInset as an additional padding extending from the content area edges.

Here's an illustration on how contentInset works, and how to use it to adapt for the keyboard.

Move view when keyboard is shown (guide)

When the keyboard will show / will hide notification is observed, we can modify the contentInsets of the scrollview :

// ViewController.swift
class ScrollViewController: UIViewController {

  @IBOutlet weak var scrollView: UIScrollView!
    
  override func viewDidLoad() {
    super.viewDidLoad()

      // Do any additional setup after loading the view.
    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
    
    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
  }
  
  @objc func keyboardWillShow(notification: NSNotification) {
    guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
    else {
      // if keyboard size is not available for some reason, dont do anything
      return
    }

    let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: keyboardSize.height , right: 0.0)
    scrollView.contentInset = contentInsets
    scrollView.scrollIndicatorInsets = contentInsets
  }

  @objc func keyboardWillHide(notification: NSNotification) {
    let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
		
    
    // reset back the content inset to zero after keyboard is gone
    scrollView.contentInset = contentInsets
    scrollView.scrollIndicatorInsets = contentInsets
  }
}


The result will look like this :

Move view when keyboard is shown (guide)

Pretty nifty!

Download the sample keyboard move up project. (normal + scrollview)

Not sure if you are doing it right on handling the view when keyboard shows?

Download the sample project and compare it yourself!

https://github.com/fluffyes/keyboardmoveup/archive/master.zip

Further Reading

Apple official documentation on Keyboard management

]]>
<![CDATA[How to use scroll view in Interface Builder / Storyboard (Xcode 11)]]>Table of contents:

  1. Structure of scroll view
  2. Content Layout guide and Frame Layout guide
  3. Step 1: Put a scroll view into the view controller and set constraints
  4. Step 2 : Put a view inside the scroll view and set constraints
  5. Step 3: Placing UI Elements inside Content view

Before moving on

]]>
https://fluffy.es/scrollview-storyboard-xcode-11/5e09dcee2b012b0b9547f346Mon, 30 Dec 2019 11:43:20 GMT

Table of contents:

  1. Structure of scroll view
  2. Content Layout guide and Frame Layout guide
  3. Step 1: Put a scroll view into the view controller and set constraints
  4. Step 2 : Put a view inside the scroll view and set constraints
  5. Step 3: Placing UI Elements inside Content view

Before moving on to how to use a scrollview in storyboard / XIB, I think it's crucial to discuss the structure of scroll view to prevent getting "ambiguous scrollable content width / height" error message in Interface Builder.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

Structure of scrollview

Scrollview works by having a scrollable content area, like this:

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

In order for scrollview to work in Auto Layout, scrollview must know its scrollable content (scrollview content) width and height , and also the frame (X, Y , Width, Height) of itself, ie. where should the superview place scrollview and what size.

Xcode will show you the error "ambiguous scrollable content width / height" if Auto Layout doesn't know what is the width and height for the scrollable content inside scrollview.

Content Layout guide and Frame Layout guide

In Xcode 11, Apple has introduced Content Layout Guide and Frame Layout Guide for Scroll View in Interface Builder, which makes it easier to design UI inside Scroll View. This is enabled by default.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

To layout a scrollview correctly, we will need two set of constraints :

  1. Constraints that set the size and position of the scroll view relative to its superview.
  2. Constraints that set the size of the scrollable content area inside the scrollview, so the scroll view can know the scrollable content dimension and render correctly.

The Frame Layout guide relates to the position (x, y) and size (width, height) of the scrollview itself, relative to the superview it was placed in.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

The Content Layout guide relates to the size of the scrollable content area inside the scroll view.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

The challenging part is to define enough constraints to let Auto Layout calculate the scrollable content area (Content Layout) width and height, which we will go into in the next section.

Download the sample scroll view project
Not sure if you are doing it right on the scroll view layout?

Download the sample project and compare it yourself!
https://github.com/fluffyes/scrolla/archive/master.zip

Step 1: Put a scroll view into the view controller and set constraints

First, place a scroll view into the view controller, and set some constraints for it so Auto Layout can know how to position and size it.

Usually I will pin it to the edges (top, leading, trailing and bottom) of the safe area like this :

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

After setting constraints, you will notice there's red lines and errors because Auto Layout don't know the scrollable content size of the scroll view yet. No worries on this as we are going to put a view inside it next.

Step 2 : Put a view inside the scroll view and set constraints

To simplify the height calculation of the scroll content area, add a view inside the scrollview , and add four constraints (leading, trailing, top and bottom, set it to 0) from the view to the scrollview's content layout guide.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

Select the view inside scrollview, press and hold control and drag it to the content layout guide.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

Then in the constraints selection popup, hold shift to select multiple constraints, select leading / top / trailing / bottom constraint.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

After creating these constraints, edit their constant values to 0, so that the view will be pinned to the leading/top/trailing/bottom edges of the content layout guide :

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

Let's name this view as 'Content View' as this view contains UI elements (content) for the scrollview. Auto Layout will use the content view's' width and height to calculate the scrollable area width / height for the scrollview.

After changing the four constraints value to 0 , you can notice that the red lines are still there. This is because despite pinning the edge of the view to content layout guide, Auto Layout still doesn't know what is the width and height of the view.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

As we want to make the scrollview scroll only vertically (it does not scroll horizontally), the scrollable content area width should be equal or lesser than the scrollview size width. Next, we will create an equal width constraint between the view and the scrollview's frame layout guide, so the scrollable content area has the same width with the scrollview.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

Xcode might create a proportional width constraint for you, if thats the case, edit the 'proportional width' constraint multiplier value to "1" so that it will be equal width.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

Now that we have defined the width for the scrollable content area (content layout guide), what's left now is the height of the scrollable content area.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

We will set an explicit height constraint value to the content view for now. We will remove it later once we have finished placing elements in it as we want to make the scrollview have dynamic height, based on the content inside it (retrieved from API etc).

Let's set the height for the content view (the view inside scrollview) to 1000 :

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

Now we have set the width and height for the view, and Auto Layout can calculate the scrollable content area size, yay! We will remove the explicit height constraint (1000 pt) after finish adding UI elements.

You can scroll the scrollview in the Storyboard / Interface builder! Select the view inside scrollview in Document Outline, then scroll using your mouse or trackpad, you can see the view move.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

If you don't want to keep scrolling and want to see the whole scrollable content area, you can elongate the view controller height by setting it to freeform with bigger height like this :

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

  1. Select the view controller
  2. Select "Size Inspector"
  3. For "Simulated Size", select "Freeform"
  4. Set "1100" as its height

You should see the view controller elongated to 1100 pt height now. Now you can start placing labels, images etc inside the content view (the view inside scrollview). After placing, be sure to create constraints from the top element to the bottom. You can increase the height of view controller in simulated size and height constraint of the content view as you need.

We will go into detail on how to create constraints from top to bottom inside the content view in the next section.

Step 3: Placing UI Elements inside Content view

Here's how it looks like after placing labels and image inside the content view in scrollview:

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

You can start placing elements inside the content view like label, images ,etc. Then add constraints for them so that Auto Layout can calculate their X position, Y position, width and height (can skip width and height if they have intrinsic content size). Remember that the constraints should be between each UI elements and the content view (ie. the view inside scrollview), not with the scrollview.

Your constraints might look like this:

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

Remember to set enough constraints so that Auto Layout can calculate the scrollable content height after removing the explicit height.

How to use scroll view in Interface Builder / Storyboard (Xcode 11)

The important note is that the vertical constraints must be connected from the top edge of content view to the bottom edge of content view, this is to ensure Auto Layout can calculate the scrollable content area height.

Now remove the explicit height constraint set on the content view, you should get all blue lines if you have set the constraints correctly.

After setting up constraints, you can create outlet for the labels and set the text value to the JSON value gotten from web API. Your scrollable content area height will expand based on the intrinsic content size of the labels.

This article is an excerpt from the book Making Sense of Auto Layout , if you feel like you have no idea what you are doing when adding / removing / editing constraints,  give the sample chapter a try!

Download the sample scroll view project
Not sure if you are doing it right on the scroll view layout?

Download the sample project and compare it yourself!
https://github.com/fluffyes/scrolla/archive/master.zip

]]>