肇鑫的技术博客

肇鑫 / Owen Zhao

独立开发者,主要开发 iOS、watchOS、macOS 应用。

目前在维护 SleepTapRooster Time,以及 Markdown Writer 相关工具。

最新文章

SwiftUI, Core Data and CloudKit Part 1

Core Data

This is a simple project for SwiftUI, Core Data and CloudKit. In part 1, I will create a simple project in SwiftUI, then turn it into Core Data. In part 2, I will use CloudKit to sync with Core Data.

SwiftUI

Create a macOS SwiftUI project.

Project Code

//
//  ContentView.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

import SwiftUI

struct ContentView: View {
    @State private var items = [Item]()
    @State private var addNewItem = false
    
    let addNewItemPublisher = NotificationCenter.default.publisher(for: AddItemView.addNewItem)
    
    var body: some View {
        VStack {
            ScrollView(.vertical, showsIndicators: true) {
                if items.isEmpty {
                    Text("No Data")
                } else {
                    List($items) { item in
                        HStack {
                            Text(DateFormatter.localizedString(from: item.startDate.wrappedValue, dateStyle: .none, timeStyle: .short))
                            Text(item.title.wrappedValue)
                        }
                    }
                    .frame(minWidth: 580, minHeight: 400, idealHeight: 600)
                }
            }
            .onReceive(addNewItemPublisher) { notification in
                if let userInfo = notification.userInfo as? [String:Item], let item = userInfo["new item"] {
                    items.append(item)
                    print(items)
                }
            }
            
            Button {
                add()
            } label: {
                Text("Add")
            }
            .sheet(isPresented: $addNewItem) {
                AddItemView()
            }
        }
        .padding()
        .frame(width: 600, height: 600, alignment: .center)
        
    }
    
    private func add() {
        addNewItem = true
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
//
//  AddItemView.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

import SwiftUI

struct AddItemView: View {
    static let addNewItem = Notification.Name("addNewItem")
    
    @Environment(\.dismiss) private var dismiss
    @State private var item = Item(startDate: Date(), title: "")
    
    var body: some View {
        VStack {
            Text("Add New Item")
                .font(.title2)
            
            TextField("What to do?", text: $item.title, prompt: Text("Go shopping."))
            DatePicker("When?", selection: $item.startDate)
            
            HStack {
                Button {
                    save()
                } label: {
                    Text("Save")
                }
                .disabled(item.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)

                Spacer()
                
                Button {
                    cancel()
                } label: {
                    Text("Cancel")
                }
            }
        }
        .padding()
    }
    
    private func save() {
        NotificationCenter.default.post(name: AddItemView.addNewItem, object: nil, userInfo: ["new item" : item])
        dismiss()
    }
    
    private func cancel() {
        dismiss()
    }
}

struct AddItemView_Previews: PreviewProvider {
    static var previews: some View {
        AddItemView()
    }
}
//
//  Model.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

import Foundation

struct Item:Identifiable {
    let id = UUID().uuidString
    var startDate:Date
    var title:String
}

class ItemProvider:ObservableObject {
    @Published var items = [Item]()
}

You can test and run the app.

Core Data Basics

NSPersistentContainer

NSPersistentContainer handles the creation of the Core Data stack and offers access to the NSManagedObjectContext as well as a number of convenience methods.

NSManagedObjectModel

The NSManagedObjectModel instance describes the data that is going to be accessed by the Core Data stack. During the creation of the Core Data stack, the NSManagedObjectModel is loaded into memory as the first step in the creation of the stack.

NSPersistentStoreCoordinator

The NSPersistentStoreCoordinator sits in the middle of the Core Data stack. The coordinator is responsible for realizing instances of entities that are defined inside of the model. It creates new instances of the entities in the model, and it retrieves existing instances from a persistent store (NSPersistentStore).

NSManagedObjectContext

The managed object context (NSManagedObjectContext) is the object that your application will interact with the most, and therefore it is the one that is exposed to the rest of your application. Think of the managed object context as an intelligent scratch pad. When you fetch objects from a persistent store, you bring temporary copies onto the scratch pad where they form an object graph (or a collection of object graphs). You can then modify those objects however you like. Unless you actually save those changes, however, the persistent store remains unaltered.

All managed objects must be registered with a managed object context. You use the context to add objects to the object graph and remove objects from the object graph. The context tracks the changes you make, both to individual objects’ attributes and to the relationships between objects. By tracking changes, the context is able to provide undo and redo support for you. It also ensures that if you change relationships between objects, the integrity of the object graph is maintained.

If you choose to save the changes you have made, the context ensures that your objects are in a valid state. If they are, the changes are written to the persistent store (or stores), new records are added for objects you created, and records are removed for objects you deleted.

Without Core Data, you have to write methods to support archiving and unarchiving of data, to keep track of model objects, and to interact with an undo manager to support undo. In the Core Data framework, most of this functionality is provided for you automatically, primarily through the managed object context.

Core Data

Project Code

To use core data, first we must remove "Model.swift" file. Then we created a Core Data Model file, named it as "Model.xcdatamodeld".

Then we create it attributes.

core data model

Create ItemProvider

Add a new file, name it as "ItemProvider.swift".

//
//  ItemProvider.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

import Foundation
import CoreData
import AppKit

class ItemProvider:ObservableObject {
    let container = NSPersistentContainer(name: "Model")
    
    init() {
        container.loadPersistentStores { description, error in
            if let error = error {
                let alert = NSAlert(error: error)
                NSSound.beep()
                alert.runModal()
            }
        }
    }
}

We must first create the NSPersistentContainer, or the app will not know how to create an instance of "Item".

Core Data in SwiftUI

We need to set managedObjectContext in environment, so it will be use in the SwiftUI app.

//
//  Todo_ListApp.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

/*
 <a href="https://www.flaticon.com/free-icons/reminder" title="reminder icons">Reminder icons created by max.icons - Flaticon</a>
 */

import SwiftUI

@main
struct Todo_ListApp: App {
    @StateObject private var itemProvider = ItemProvider()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, itemProvider.container.viewContext)
        }
    }
}

Then in ContentView.swift, we fetch the results from Core Data.

//
//  ContentView.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

import SwiftUI

struct ContentView: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @FetchRequest(sortDescriptors: [SortDescriptor(\.startDate, order: .forward)]) var items:FetchedResults<Item>
    @State private var item:Item?
    
    var body: some View {
        VStack {
            ScrollView(.vertical, showsIndicators: true) {
                if items.isEmpty {
                    Text("No Data")
                } else {
                    List(items) { item in
                        HStack {
                            Text(DateFormatter.localizedString(from: item.startDate!, dateStyle: .none, timeStyle: .short))
                            Text(item.title!)
                        }
                    }
                    .frame(minWidth: 580, minHeight: 400, idealHeight: 600)
                }
            }
            
            Button {
                add()
            } label: {
                Text("Add")
            }
            .sheet(item: $item) { item in
                AddItemView(item: item)
            }
        }
        .padding()
        .frame(width: 600, height: 600, alignment: .center) 
    }
    
    private func add() {
        let item = Item(context: managedObjectContext)
        item.id = UUID()
        item.startDate = Date()
        self.item = item
    }
}

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

In AddItemView, the similar.

//
//  AddItemView.swift
//  Todo List
//
//  Created by zhaoxin on 2022/5/7.
//

import SwiftUI
import CoreData

struct AddItemView: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @Environment(\.dismiss) private var dismiss
    @State var item:Item
    
    var body: some View {
        VStack {
            Text("Add New Item")
                .font(.title2)
            
            TextField("What to do?", text: Binding($item.title) ?? .constant(""), prompt: Text("Go shopping."))
            DatePicker("When?", selection: Binding($item.startDate) ?? .constant(Date()))
            
            HStack {
                Button {
                    save()
                } label: {
                    Text("Save")
                }

                Spacer()
                
                Button {
                    cancel()
                } label: {
                    Text("Cancel")
                }
            }
        }
        .padding()
    }
    
    private func save() {
        if var title = item.title {
            title = title.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !title.isEmpty else {
                let alert = NSAlert()
                alert.messageText = NSLocalizedString("Title is empty!", comment: "")
                alert.alertStyle = .warning
                alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
                NSSound.beep()
                alert.runModal()
                
                return
            }
            
            do {
                item.title = title
                try managedObjectContext.save()
            } catch {
                let alert = NSAlert(error: error)
                NSSound.beep()
                alert.runModal()
            }
        }
        
        dismiss()
    }
    
    private func cancel() {
        managedObjectContext.rollback()
        dismiss()
    }
}

In SwiftUI, we use "NotificationCenter" to post a notification in save() function. In Core Data, we no longer need the notification as we could save directly and the change will automatically trigger the FetchedResults.

Reference

App Architecture of SwiftUI

SwiftUI

When developing with Cocoa/UIKit, the most common used architecture is MVC.

model_view_controller_2x

What is the architecture when using SwiftUI? Someone says it is MVVM. Someone says it is MV, MVC without C. I would like say it would be M-VC, Model with View and Controller.

M-VC

We want every element to be simple. For MV architecture, the View is too heavy for me. For M-VC, we want the View as simple as possible. The extra parts we think it to be Controller.

Split View into small Views

For a SwiftUI View, when one of the @State properties changed, all @State properties in the same view will be updated. If this is overhead, you can split those @State properties to another view, that will reduce CPU usage and speed up your app.

Get Macs, IP Addresses and Internet IP Addresses

通用

We need to use C function to get those values. And then we check the values with command ifconfig and understand which to which.

Get Local MACs and IP Addresses

private func getLocalIPAndMACAdress() {
    var address : String?

    // Get list of all interfaces on the local machine:
    var ifaddr:UnsafeMutablePointer<ifaddrs>!
    if getifaddrs(&ifaddr) == 0 {
        // For each interface ...
        var ptr:UnsafeMutablePointer<ifaddrs>! = ifaddr
        var networkLinkDictionary = [String:[String]]()
        
        repeat {
            defer { ptr = ptr.pointee.ifa_next}
            let interface = ptr.pointee
            // Check interface name:
            let name = String(cString: interface.ifa_name)
            // Convert interface address to a human readable string:
            var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
            getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len),
                        &hostname, socklen_t(hostname.count),
                        nil, socklen_t(0), NI_NUMERICHOST)
            address = String(cString: hostname)
            var values = networkLinkDictionary[name] ?? []
            values.append(address!)
            networkLinkDictionary.updateValue(values, forKey: name)
        } while ptr != nil
   
        freeifaddrs(ifaddr)
        
        set(networkLinkDictionary: networkLinkDictionary)
        
        #if DEBUG
        networkLinkDictionary.keys.sorted().forEach { key in
            print(key, "\t", networkLinkDictionary[key]!)
        }
        #endif
    }
}

private func set(networkLinkDictionary:[String:[String]]) {
    networkLinkDictionary.keys.sorted().forEach {
        switch $0 {
            // for iMac 5K, en0 is ethernet; en1 is WiFi
        case "en0":
            enthernet = getNetworkLink(values: networkLinkDictionary["en0"] ?? [])
            #if DEBUG
            debugPrint(enthernet)
            #endif
        case "en1":
            wifi = getNetworkLink(values: networkLinkDictionary["en1"] ?? [])
            #if DEBUG
            debugPrint(wifi)
            #endif
        default:
            break
        }
    }
}

private func getNetworkLink(values:[String]) -> NetworkLink {
    let networkLink:NetworkLink
    
    switch values.count {
    case 1:
        networkLink = NetworkLink(MAC: values[0])
    case 2:
        networkLink = NetworkLink(MAC: values[0], ipv6: values[1])
    case 3:
        networkLink = NetworkLink(MAC: values[0], ipv6: values[1], ipv4: values[2])
    default:
        networkLink = NetworkLink(MAC: "")
    }
    
    return networkLink
}

public struct NetworkLink:Equatable {
    public init(MAC:String, ipv6:String? = nil, ipv4:String? = nil){
        self.MAC = MAC
        self.ipv6 = ipv6
        self.ipv4 = ipv4
    }
    
    public let MAC:String
    public var ipv6:String? = nil
    public var ipv4:String? = nil
}

Get Internet IP Addresses

We use ipify.org to get internet IP addresses.

Don't use URLSession.share

Create a new URLSession each time your own. If you use URLSession.share, you app won't detect internet IP changes, it will always show the IP when the app starts.

I had tried 3 different frameworks on github.com. They all has the above issue. So I write the code myself.

private func setInterIP(type:IPType) async {
    let url:URL
    
    if !reachable {
        switch type {
        case .ipv4:
            internetIPV4 = NSLocalizedString("Inactive", bundle: .module, comment: "")
        case .ipv6:
            internetIPV6 = NSLocalizedString("Inactive", bundle: .module, comment: "")
        }
        
        return
    }
    
    switch type {
    case .ipv4:
        url = URL(string: "https://api.ipify.org?format=json")!
        
        if internetIPV4.isEmpty {
            internetIPV4 = "..."
        }
    case .ipv6:
        url = URL(string: "https://api64.ipify.org?format=json")!
        
        if internetIPV6.isEmpty {
            internetIPV6 = "..."
        }
    }
    let urlSessionConfiguration = URLSessionConfiguration.default
    let urlSession = URLSession(configuration: urlSessionConfiguration)
    
    do {
        let (data, urlResponse) = try await urlSession.data(from: url)
        DispatchQueue.main.async { [self] in
            if let httpResponse = urlResponse as? HTTPURLResponse, httpResponse.statusCode == 200 {
                if let dic = try? JSONSerialization.jsonObject(with: data, options: []) as? [String:String] {
                    switch type {
                    case .ipv4:
                        internetIPV4 = dic["ip"]!
                        #if DEBUG
                        debugPrint(internetIPV4)
                        #endif
                    case .ipv6:
                        internetIPV6 = dic["ip"]!
                        #if DEBUG
                        debugPrint(internetIPV6)
                        #endif
                    }
                }
            } else {
                #if DEBUG
                debugPrint(urlResponse)
                #endif
            }
        }
    } catch {
        print(error)
    }
}

Fix of Xcode SPM Downloading Speed Issue

Vapor

This command will fixed the issue that Xcode downloading too slow with SPM. Say you have a project name "hello", there is a Package.swift in it.

$ cd hello
$ xcodebuild -resolvePackageDependencies -scmProvider system
$ open Package.swift

hello

This issue was because when using git, Xcode ignore the proxy setting by default.

Pre-request

You should have your proxy set, for me, I have

$ cat .bashrc 
# proxy list
alias proxy='export all_proxy=socks5://127.0.0.1:7890'
alias unproxy='unset all_proxy'

# run
proxy

Xcode not follows system proxy settings to resolve SwiftPM dependencies

Xcode 13 ignores GIT proxy settings when resolving SwiftPM packages (and probably for other SCM operations as well).

Vapor with SQLite

Vapor

When using SQLite driver with Vapor, we have to install another package, or the project won't compile.

$ sudo apt install libsqlite3-dev

Vapor Behind NGINX in Ubuntu 20.04

Vapor

  1. Install developing packages.

How To Install Vapor On A Virtual Machine

  1. Install NGINX
  2. Install Swift
  3. Install Vapor Toolbox
  4. Check RAM
  5. Create Vapor Sample Project
  6. Set Vapor Proxy Behind NGINX
  7. Install SuperVisor

My server was 1GB RAM VPS in Ubuntu 20.04 LTS. And I developed vapor on my iMac 5K. So the first thing I did was to create a Linux client.

I prefer VMWare Fusion. As it was free for open source use. I tried VirtualBox, it ran slow with iMac 5K. Parallels Desktop was also good. But it was too expensive.

After all, now you should have a working Ubuntu 20.04.

Install developing packages.

 $ sudo apt update
 $ sudo apt install \
          binutils \
          git \
          gnupg2 \
          libc6-dev \
          libcurl4 \
          libedit2 \
          libgcc-9-dev \
          libpython2.7 \
          libsqlite3-0 \
          libstdc++-9-dev \
          libxml2 \
          libz3-dev \
          pkg-config \
          tzdata \
          zlib1g-dev

Install NGINX

$ sudo apt install nginx

Open Firefox with "localhost".

Screenshot from 2021-12-02 18-48-53

Install Swift

Download the latest release Swift Toolchain for Ubuntu 20.04.

$ cd Downloads/
$ wget https://download.swift.org/swift-5.5.1-release/ubuntu2004/swift-5.5.1-RELEASE/swift-5.5.1-RELEASE-ubuntu20.04.tar.gz
$ tar xzf swift-5.5.1-RELEASE-ubuntu20.04.tar.gz
$ vi ~/.bashrc

Append

export PATH=/home/zhaoxin/Downloads/swift-5.5.1-RELEASE-ubuntu20.04/usr/bin:”${PATH}”

Save and Quit。

Quit Terminal and Open it again. Type

$ swift --version
Swift version 5.5.1 (swift-5.5.1-RELEASE)
Target: x86_64-unknown-linux-gnu

Install Vapor Toolbox

$ cd ~/Downloads
$ git clone https://github.com/vapor/toolbox.git
$ cd toolbox/
$ git checkout 18.3.3
$ swift build -c release --disable-sandbox --enable-test-discovery
$ sudo mv .build/release/vapor /usr/local/bin

Check RAM

When compile, Vapor needs at least 2GB RAM. So for some VPS with only 1GB RAM, you will get fatalError when compile. You can check RAM with command

$ free -h
              total        used        free      shared  buff/cache   available
Mem:          3.8Gi       1.2Gi       447Mi        14Mi       2.1Gi       2.3Gi
Swap:         923Mi       1.0Mi       921Mi

Above is my VMWare Fusion client. However, for you VPS, you may get a result as below

$ free -h
              total        used        free      shared  buff/cache   available
Mem:          981Mi       215Mi        95Mi       2.0Mi       670Mi       575Mi
Swap:            0B          0B          0B

If that is your case, you must create a swap or you get your Vapor project compiled.

Create Swap File

How To Add Swap Space on Ubuntu 20.04

Vapor Hello Sample Won’t Compile

$ sudo swapon --show

You may get nothing shown as there was no swap for you VPS. Check you disk free space.

df -h
Filesystem      Size  Used Avail Use% Mounted on
udev            447M     0  447M   0% /dev
tmpfs            99M  944K   98M   1% /run
/dev/vda1        24G   12G   11G  51% /
tmpfs           491M     0  491M   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           491M     0  491M   0% /sys/fs/cgroup
/dev/loop0       43M   43M     0 100% /snap/certbot/1514
/dev/loop1       43M   43M     0 100% /snap/certbot/1582
/dev/loop2      100M  100M     0 100% /snap/core/11798
/dev/loop3      100M  100M     0 100% /snap/core/11993
/dev/loop4       62M   62M     0 100% /snap/core20/1169
/dev/loop5       62M   62M     0 100% /snap/core20/1242
tmpfs            99M     0   99M   0% /run/user/0

Look at "/dev/vda1 24G 12G 11G 51% /", That meant you had 11G free space.

Create swap file.

$ sudo fallocate -l 1G /swapfile
$ ls -lh /swapfile
-rw-r--r-- 1 root root 1.0G Apr 25 11:14 /swapfile

Enable swap file

$ sudo chmod 600 /swapfile
$ ls -lh /swapfile
-rw------- 1 root root 1.0G Apr 25 11:14 /swapfile
$ sudo mkswap /swapfile
Setting up swapspace version 1, size = 1024 MiB (1073737728 bytes)
no label, UUID=6e965805-2ab9-450f-aed6-577e74089dbf
$ sudo swapon /swapfile
$ sudo swapon --show
NAME      TYPE  SIZE USED PRIO
/swapfile file 1024M   0B   -2
$ free - h
              total        used        free      shared  buff/cache   available
Mem:        1004716      166880      147152        2724      690684      643212
Swap:       1048572        5940     1042632

Save Swap File

$ sudo cp /etc/fstab /etc/fstab.bak
$ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Create Vapor Sample Project

$ cd
$ mkdir vapor
$ cd vapor
$ git config --global user.email "you@email.com"
$ git config --global user.name "Your Name"
$ vapor new hello -n
$ cd hello/
$ swift run
...
[1686/1686] Build complete!
[ NOTICE ] Server starting on http://127.0.0.1:8080

Open Firefox and type "localhost:8080".
Screenshot from 2021-12-02 19-53-32

Set Vapor Proxy Behind NGINX

403 forbidden behind nginx

$ cd /etc/nginx/sites-available/
$ sudo cp default default.bak
$ sudo rm default
$ sudo vi default

Copy and paste below to default and save.

server {
        listen 80 default_server;
        listen [::]:80 default_server;

root /var/www/html;

index index.html index.htm index.nginx-debian.html;

server_name _;
        try_files $uri @proxy;
        location @proxy {
                proxy_pass http://127.0.0.1:8080;
                proxy_pass_header Server;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_pass_header Server;
                proxy_connect_timeout 3s;
                proxy_read_timeout 10s;
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                #try_files $uri $uri/ =404;
        }
}

Restart NGINX.

sudo systemctl restart nginx

How To Install Vapor On A Virtual Machine

Install SuperVisor

$ sudo apt install supervisor
$ cd /etc/supervisor/conf.d/
$ sudo vi hello.conf

Copy and paste below code and save.

[program:hello]
command=/home/zhaoxin/vapor/hello/.build/debug/Run serve --env production
directory=/home/zhaoxin/vapor/hello
user=zhaoxin
stdout_logfile=/var/log/supervisor/%(program_name)-stdout.log
stderr_logfile=/var/log/supervisor/%(program_name)-stderr.log
$ sudo supervisorctl reread
$ sudo supervisorctl add hello
$ sudo supervisorctl start hello

Supervisor

A Little Research on How PasscodeLock Works

PasscodeLock is the sample project of AppLocker. I took some hours and found how it worked.

Let The App Run

The original project used Carthage to manage the framework. It depended Valet. However, the latest Valet is 4.x but PasscodeLock depends 3.x. So we need to specify the version.

pod 'Valet' , '< 4.0'

I switched Carthage to CocoaPods. So I also had to remove the Carthage part in building phase.

Core

sc

// AppALConstants.swift
// MARK: - Keyboard
@IBAction func keyboardPressed(_ sender: UIButton) {
    switch sender.tag {
    case ALConstants.button.delete.rawValue:
        drawing(isNeedClear: true)
    case ALConstants.button.cancel.rawValue:
        clearView()
        dismiss(animated: true) {
            self.onSuccessfulDismiss?(nil)
        }
    default:
        drawing(isNeedClear: false, tag: sender.tag)
    }
}

private func drawing(isNeedClear: Bool, tag: Int? = nil) { // Fill or cancel fill for indicators
    let results = pinIndicators.filter { $0.isNeedClear == isNeedClear }
    let pinView = isNeedClear ? results.last : results.first
    pinView?.isNeedClear = !isNeedClear
    
    UIView.animate(withDuration: ALConstants.duration, animations: {
        pinView?.backgroundColor = isNeedClear ? .clear : .white
    }) { _ in
        isNeedClear ? self.pin = String(self.pin.dropLast()) : self.pincodeChecker(tag ?? 0)
    }
}

private func pincodeChecker(_ pinNumber: Int) {
    if pin.count < ALConstants.maxPinLength {
        pin.append("\(pinNumber)")
        if pin.count == ALConstants.maxPinLength {
            switch mode {
            case .create:
                createModeAction()
            case .change:
                changeModeAction()
            case .deactive:
                deactiveModeAction()
            case .validate:
                validateModeAction()
            }
        }
    }
}

When a user presses a key, private func drawing(isNeedClear: Bool, tag: Int? = nil) is called.

  1. pinView is the current indicator. It is set to turn white.
  2. then func pincodeChecker(_ pinNumber: Int) is called, the result is pin.
  3. when pin.count is 4, the indicators are cleared.

UISplitViewController in Modern UiKit

iOS

I have a review and find this article is the fourth time that I am talking about UISplitViewController. That is because UISplitViewController is hard and UISplitViewController changes.

Finally, this time we will only talk the modern part of UISplitViewController, which means those talks are base on iOS 14 and later.

UISplitViewController In Generalui-split-view-overview@2x-w589.5

Above picture is from Apple's document site.

UISplitViewController

The typical app of UISplitViewController is the Mail app in app.

You should be aware that the order of the ViewControllers. There are primary, supplementary and secondary.
While, in Xcode, the orders are different.

xcode_segues

Whenever you find an unknown bug, first make sure you have one and only one UINavigationViewController that is in the primary ViewController.

Goal

Our goal is to have an app all in blue. So first I added some data to it.

xcode_storyboard

First, I choose all background to system blue in storyboard. Then added data.

PrimaryTableViewController.swift

//
//  PrimaryTableViewController.swift
//  UISplitView Sample
//
//  Created by zhaoxin on 2021/10/21.
//

import UIKit

class PrimaryTableViewController: UITableViewController {
    private var items:[Int] = (1...3).map { $0 }

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // MARK: - Table view data source
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "mainCell", for: indexPath)
        let item = items[indexPath.row]
        cell.textLabel?.text = String(item)
        
        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if let splitViewController = self.splitViewController {
            splitViewController.show(.supplementary)
        }
    }
}

SupplementaryViewController.swift

//
//  SupplementaryViewController.swift
//  UISplitView Sample
//
//  Created by zhaoxin on 2021/10/21.
//

import UIKit

class SupplementaryViewController: UIViewController {
    private var items:[Int] = (1...5).map { $0 }

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

extension SupplementaryViewController:UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "supportCell", for: indexPath)
        let item = items[indexPath.row]
        cell.textLabel?.text = String(item)
        
        return cell
    }
}

extension SupplementaryViewController:UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if let splitViewController = self.splitViewController {
            splitViewController.show(.secondary)
        }
    }
}

SecondViewController.swift

//
//  SecondViewController.swift
//  UISplitView Sample
//
//  Created by zhaoxin on 2021/10/21.
//

import UIKit

class SecondViewController: UIViewController {
    private var items:[Int] = (1...10).map { $0 }

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

extension SecondViewController:UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let item = items[indexPath.row]
        cell.textLabel?.text = String(item)
        
        return cell
    }
}

The app run like this.

sc_01

As you can see, the color of the title bar is not blue.

We need to add an image to background of the navigationBar.

UIColor+Image.swift

//
//  UIColor+Image.swift
//  Any Counter 2
//
//  Created by zhaoxin on 2021/10/20.
//  Copyright © 2021 ParusSoft.com. All rights reserved.
//

import Foundation
import UIKit

extension UIImage {
    static func from(color: UIColor) -> UIImage {
        let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
        UIGraphicsBeginImageContext(rect.size)
        let context = UIGraphicsGetCurrentContext()
        context!.setFillColor(color.cgColor)
        context!.fill(rect)
        let img = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return img!
    }
}

SecondViewController.swift

//
//  SecondViewController.swift
//  UISplitView Sample
//
//  Created by zhaoxin on 2021/10/21.
//

import UIKit

class SecondViewController: UIViewController {
    private var items:[Int] = (1...10).map { $0 }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        navigationController?.navigationBar.setBackgroundImage(UIImage.from(color: .systemBlue), for: .default)
    }
}

extension SecondViewController:UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let item = items[indexPath.row]
        cell.textLabel?.text = String(item)
        
        return cell
    }
}

The title bar turned to blue when the device was landscape. But in portrait, the color didn't change. Also, there was extra space in portrait.

sc_02-w1,104

sc_03

View Hierarchy

xcode_views-w1,565
We captured the portrait view controller in Xcode and looked into the view's hierarchy. We found that there were two instead of one UINavigationController used.

UISplitViewController is complex and hard to predict. Because it is combined states of two UINavigationViewControllers.
So each time we encounter a problem, we must consider the current state of the two UINavigationViewControllers.

Solution

First, we wanted all UINavigationController turn blue. So we do it in the UISplitViewController.

SplitViewController.swift

//
//  SplitViewController.swift
//  UISplitView Sample
//
//  Created by zhaoxin on 2021/10/21.
//

import UIKit

class SplitViewController: UISplitViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        children.forEach {
            if let nav = $0 as? UINavigationController {
                nav.navigationBar.setBackgroundImage(UIImage.from(color: .systemBlue), for: .default)
            }
        }
    }
}

And remove the previous changes in SecondViewController.swift.

sc_04-621

Now the portrait view controller was blue, however, the gap was still there.

When you set both UINavigationViewControllers, the space doubles in portrait mode.
You must to hide a second UINavigationViewController to remove the space.
However, isHidden of navigationBar didn't work here.
You have to set background image to nil to hide the UINavigationViewController.

SecondViewController.swift

//
//  SecondViewController.swift
//  UISplitView Sample
//
//  Created by zhaoxin on 2021/10/21.
//

import UIKit

class SecondViewController: UIViewController {
    private var items:[Int] = (1...10).map { $0 }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let frame = view.frame
        if frame.height > frame.width {
            self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
        }
    }
}

extension SecondViewController:UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let item = items[indexPath.row]
        cell.textLabel?.text = String(item)
        
        return cell
    }
}

Now the gap was gone. However, when rotating, the title went blank again.
sc_05-621

sc_06

set background image when screen rotate

SecondViewController.swift

//
//  SecondViewController.swift
//  UISplitView Sample
//
//  Created by zhaoxin on 2021/10/21.
//

import UIKit

class SecondViewController: UIViewController {
    private var items:[Int] = (1...10).map { $0 }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let frame = view.frame
        if frame.height > frame.width {
            self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
        }
    }

    // MARK: - rotate
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)

        if size.height > size.width {
            self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
        } else {
            self.navigationController?.navigationBar.setBackgroundImage(UIImage.from(color: .systemBlue), for: .default)
        }
    }
}

extension SecondViewController:UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let item = items[indexPath.row]
        cell.textLabel?.text = String(item)
        
        return cell
    }
}

SupplementaryViewController.swift

//
//  SupplementaryViewController.swift
//  UISplitView Sample
//
//  Created by zhaoxin on 2021/10/21.
//

import UIKit

class SupplementaryViewController: UIViewController {
    private var items:[Int] = (1...5).map { $0 }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let frame = view.frame
        if frame.height > frame.width {
            self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
        }
    }
    

    // MARK: - rotate
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)

        if size.height > size.width {
            self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
        } else {
            self.navigationController?.navigationBar.setBackgroundImage(UIImage.from(color: .systemBlue), for: .default)
        }
    }

}

extension SupplementaryViewController:UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "supportCell", for: indexPath)
        let item = items[indexPath.row]
        cell.textLabel?.text = String(item)
        
        return cell
    }
}

extension SupplementaryViewController:UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if let splitViewController = self.splitViewController {
            splitViewController.show(.secondary)
        }
    }
}

TintColor Lost When Landscape

There was a bug on landscape. TintColor set in storyboard was lost.

SplitViewController.swift

//
//  SplitViewController.swift
//  UISplitView Sample
//
//  Created by zhaoxin on 2021/10/21.
//

import UIKit

class SplitViewController: UISplitViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        children.forEach {
            if let nav = $0 as? UINavigationController {
                nav.navigationBar.setBackgroundImage(UIImage.from(color: .systemBlue), for: .default)
                nav.navigationBar.tintColor = .systemYellow
            }
        }
    }
}

Sample Project

*UISplitView Sample