肇鑫的技术博客

业精于勤,荒于嬉

NSOutlineView Briefing

The new version of Xliff Tool needs to use NSOutlineView. However, the official document by Apple is of flaw and the answers on SO are too old. I have to pay nearly a whole afternoon to find out what the right approach is. Here is the briefing.

Briefing

In fact, Apple does provide a sample project of NSOutlineView. However, that sample is based on cocoa bindings. So if you want to know cocoa bindings. Here is the way.

Navigating Hierarchical Data Using Outline and Split Views

Otherwise, welcome to go on.

On Apple's Docs, it says:

If you are using conventional data sources for content you must implement the basic methods that provide the outline view with data: outlineView(_:child:ofItem:), outlineView(_:isItemExpandable:), outlineView(_:numberOfChildrenOfItem:), and outlineView(_:objectValueFor:byItem:). Applications that acquire their data using Cocoa bindings do not need to implement these methods.

However, if you create a new project with the Xcode 12.4 with Storyboard, the sample you find from SO won't work.

That is because the document is outdated.

Steps

  1. Create a new project, using Swift and Storyboard.
  2. Add a Source List to the view controller.

The result will be look like this.

outline_view_in_view_controller

Right click the storyboard and choose "Outline View". You will find the Outline View is View based.

outline_view_is_view_based_by_default

That is the reason why Apple's document doesn't work. You need to change View based to Cell based to make it work.

codes

//
//  ViewController.swift
//  OutlineView Sample
//
//  Created by zhaoxin on 2021/3/20.
//

import Cocoa

class ViewController: NSViewController {
    @IBOutlet weak var outlineView: NSOutlineView!
    
    private let cats:[String] = {
        (1...3).map {
            return String(format: "cat%02d", $0)
        }
    }()
    
    private let dogs:[String] = {
        (1...4).map {
            return String(format: "dog%02d", $0)
        }
    }()
    
    lazy private var animals = [cats, dogs]
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }


}

extension ViewController:NSOutlineViewDataSource {
    // 2
    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        if item == nil {
            return animals[index]
        }
        
        if let list = item as? [String] {
            return list[index]
        }
        
        return item as! String
    }
    
    // 3
    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        if item is [String] {
            return true
        }
        
        return false
    }
    
    // 1
    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        if item == nil {
            return animals.count
        }
        
        if let list = item as? [String] {
            return list.count
        }
        
        return 0
    }
    
    // 4
    // This function only works with Cell based Outline Views.
//    func outlineView(_ outlineView: NSOutlineView, objectValueFor tableColumn: NSTableColumn?, byItem item: Any?) -> Any? {
//
//        if item is [String] {
//            let row = outlineView.row(forItem: item)
//
//            if row == 0 {
//                return "cats"
//            }
//
//            return "dogs"
//        }
//
//        return item as! String
//    }
}

extension ViewController:NSOutlineViewDelegate {
    // 4
    // This function only works with View based Outline Views.
    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        let view:NSTableCellView!

        if item is [String] {
            view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("HeaderCell"), owner: self) as? NSTableCellView

            let row = outlineView.row(forItem: item)

            if row == 0 {
                view.textField?.stringValue = "cats"
            } else {
                view.textField?.stringValue = "dogs"
            }
        } else {
            view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("DataCell"), owner: self) as? NSTableCellView
            view.textField?.stringValue = item as! String
        }

        return view
    }
}

Look at the order number of the function, that is the order the system calls them.

  1. At first, the system asks how many children of root.
  2. Then the system asks you to provide the object that represent the child.
  3. Then the system asks you if the child is expandable.
  4. Then the system asks you to provide the view/cell of the child.
  5. When you click a child, the system goes to step 1, but this time, the child is the new root.

You should aware that there are two kinds of items. One is Any?, the other is Any.
They are different.
The Any? type is the parent node of the child. For first level cells, item is nil. nil means it is the root.
The Any type is the child.

References

My Sample

Apple's Document Outline View

NSOutlineView example on SO

Mac OSX 开发基础控件学习之 NSOutlineView

Speed up Xcode Building Time with CocoaPods for Beginners

At my first thought, this article had supposed to be named "The Modern Guide of Carthage with Xcode 12 for Beginners". However, while the process of learning Carthage and replace my own projects from using CocoaPods to Carthage. I found that using Carthage alone was not enough, or too complex for a one-week-learner like me.

The purpose of this article is to speed up your Xcode building and archiving time. In my own test, the speed could be over the original 10 times.

Since I am a beginner for Carthage, I may change this article a lot with updates.

Frameworks

A Framework is a prebuilt code package that you can use in your own code.

As programmers, we use frameworks a lot. For example, when you import UIKit, you are using UIKit framework provided by Apple.

UIKit is a build-in framework, so you just need to import it. There are also many other frameworks provided by third-parties. You can even create your own frameworks. For third-party frameworks, it is your responsibility to let Xcode know how to using them.

You can manually drag and drop those frameworks to Xcode. Or you can use framework tools like CocoaPods or Carthage.

CocoaPods VS Carthage

CocoaPods is more widely used. It has a long history ever since Objective-c. It uses source code so it has the most compatibility. Since you have to compile the framework from source code every time, it takes more time when build and archive.

Unless CocoaPods, Carthage builds the frameworks while you download them. Then you can drag and drop those frameworks to Xcode as if there are prebuilt. Those frameworks won't need to recompile again as they are already binaries. Your own project building time is saved.

We could use CocoaPods or Carthage alone, or mixed, or neither. The more you use CocoaPods, the complex the source codes are, the more build time of your own project lasts. So we intent to use Carthage alone. However, Carthage is not as compatible as CocoaPods with Xcode. Sometime you may find no solution to solve the problem of Carthage. Then you can try CocoaPods.

xcframework

Xcframework is the latest framework format that Xcode 12 supports and recommended.

Before xcframework, when we use a framework on macOS. The framework is easy to build. As for a Mac, only x86_64 architecture is used. If the framework is for iOS, things go complex. As if you could run an iOS app on both iOS simulators and iOS devices. For an iOS simulator running on a Mac, its architecture is x86_64, the same as the Mac. For an iOS device, the architecture is arm64. The framework you built should work in both x86_64 iOS simulators and arm64 iOS devices.

Framework tools use lipo to merge and split frameworks of different architectures.

$ man lipo

lipo - create or operate on universal files

A framework that contains more than one architectures is called a fat framework. Xcode could use the framework for both iOS simulators and iOS devices.

Apple Silicon Macs

Things go left when M1 Macs released. As they are arm based Macs instead of Intel based Macs. Everything on them is arm64 architecture.

For a framework runs on Mac, it now needs to be built into both x86_64 and arm64. lipo could do the trick.

For a framework runs on iOS, it now needs to be built into x86_64 and arm64 for iOS simulators and arm64 for iOS devices. You may think you can still use lipo. No, you can't. As you could see, there are arm64 in both iOS simulators and iOS devices. lipo could only specify a framework by its architecture, but it doesn't know if it is an iOS simulator or an iOS device. lipo cannot combine those frameworks together as the arm64 architectures are duplicated.


Xcframework comes. Xcframework is a bundle that contains an Info.plist with the information that the frameworks it contains. For each individual framework, it could be a fat framework or not.

Below is a picture of two built xcframeworks of Realm. The left is the Realm office prebuilt framework. The right is the framework built by Carthage by my Intel based iMac.

realm_official_VS_carthage

As we could see, for the left, there is an extra iOS maccatalyst framework. And for all simulators, there are extra arm64 architectures. I think it is because that xcframework was built on a M1 based Mac.

I didn't find the way to build arm64 architecture frameworks for simulators on an Intel based Mac. This leads to a bug of Xcode 12 on iOS app with watchOS app as companion.

Carthage by default can build xcframeworks with all architectures in all platforms, except Mac Catalyst.

Xcode 12 Requests arm64 Framework for watchOS Simulator

It is unnecessary to request arm64 framework for watchOS simulator on an Intel based Mac. It is a tradeoff. As if you are in a team that uses both Intel based and Mac based Mac, this feature is unnecessary. You can get rid of it manually.

Go to watchOS extension target -> Building Settings -> Excluded Architectures -> Debug, add "arm64".

exclude_ar

Carthage Working Steps

  1. Cartfile
  2. Resolve
  3. Checkout
  4. Build

Cartfile

Cartfile is the place where you put the framework's name and version. All frameworks that in Cartfile is called dependencies.

$ cat Cartfile
github "realm/realm-cocoa"

Resolve

When you call carthage update, carthage resolves the Cartfile as a Cartfile.resolved file. The resolved file may contain more frameworks than Cartfile as some frameworks request other frameworks.

$ cat Cartfile.resolved 
github "realm/realm-cocoa" "v10.7.2"

Checkout

Checkout downloads source codes of frameworks.

Build

The building process.

Install Frameworks to Xcode 12.4

To install frameworks to Xcode, basically we just need to drag and drop the frameworks to Xcode. However, depending the dropping locations, there are two methods of adding frameworks.

add_frameworks

Install

Cartfile

XCFrameworks

full platforms

how to installing frameworks

two ways:

  1. drag to frameworks
  2. drag to general
  3. differences

tips

remove unused framework
update arguments

known issues

  1. download binary instead of create xcframework
  2. download binary along with creation of xcframework

Xcode known issues

watchOS arm64 issue on simulator of Intel based Mac

Advances

build own framework with backups.

Strongly Recommend You to Add Your Own UINavigationControllers even in iOS 14 when Using UISplitViewController

In iOS 14, Apple adapts UISplitViewController from two columns to three columns. Especially, it says that

If you set a child view controller that’s not a navigation controller, the split view controller creates a navigation controller for it.

However, Apple didn't say that it will try to reuse the UINavigationController when you reset the columns.

That means, when you set .secondary view controller to another view controller, the split view controller will push the new view controller to the back stack, which is not what you want.

Issue Sample

UISplitViewController Issue

  1. run the sample
  2. you will see detail 01, press back
  3. you will see master 01, press back
  4. you will see primary, press Master 02
  5. you will see Master 02, press Master 02
  6. you will see Detail 02, press back

You expect to see Master 02.
You see Detail 01.

Work Around

Add a UINavigationViewController to each of detail view controllers.

UISplitViewController Issue work around

I have file this as a bug to Apple. The id is FB8640357.

Apple's Reply

Just got the reply from Apple. 8 days since I have filed this issue. Apple says:

After reviewing your feedback, we have some additional information for you:

This is expected behavior.

The secondary column gets this special treatment if the view controller gets set to a non-navigation controller when it already has a nav controller—i.e., it’s pushed onto the nav stack, as they discovered.

Expected use patterns would be:

• Change the contents of the detail view controller (not the vc itself) and send -showColumn:UISplitViewControllerColumnSecondary (if desired) to confirm that the secondary column is visible, or ...
• For the secondary column only, clear it first by setting its vc to nil, then setting new desired vc, or ...
• Manage the navigation controller in the secondary column directly with -setViewControllers: or push/pop. If you’re using an automatically created UINavigationController instead of one you provided, you can find it with -viewControllerForColumn: and then ask that vc for its navigation controller.

Apple gave 3 recommends. The first was easy to think of, but it needed the two view controllers to be very similar.

I like the second recommend most. func setViewController(_ vc: UIViewController?, for column: UISplitViewController.UISplitViewController.Column) does allow vc to be nil. And it is direct. There is some performance lose though. As it will create another UINavigationViewController each time.

The third recommend has the most performance. But it is indirect. And it smells bad. It should be seen as a workaround.

Final

I recommend to fix it in storyboard as I previously did. Or, using the second recommend by Apple.