Skip to content

Instantly share code, notes, and snippets.

@Azhrei
Last active September 18, 2018 00:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Azhrei/265ce5c12be9e3303239ebdad3a36de0 to your computer and use it in GitHub Desktop.
Save Azhrei/265ce5c12be9e3303239ebdad3a36de0 to your computer and use it in GitHub Desktop.
What is the proper use of UIResponder.undoManager (Swift 4)
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14113" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="gUO-Uk-rVh">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Item 2-->
<scene sceneID="B6D-zg-AL2">
<objects>
<viewController id="3ub-SL-PrQ" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="orx-mp-Y7e">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<viewLayoutGuide key="safeArea" id="mo3-06-6kI"/>
</view>
<tabBarItem key="tabBarItem" title="Item 2" id="Lfa-0g-MH8"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="lC5-np-aH3" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1030" y="455"/>
</scene>
<!--My SubclassedTVC-->
<scene sceneID="1Px-hQ-8Z2">
<objects>
<tableViewController id="aYp-Ls-x3G" customClass="MySubclassedTVC" customModule="TestProject" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="wcH-hv-H3C">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<sections>
<tableViewSection headerTitle="MySubclassedTVC" id="Wq8-eO-PWV">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="row1" textLabel="nCW-ld-pIh" style="IBUITableViewCellStyleDefault" id="vbN-Iv-5QF">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="vbN-Iv-5QF" id="XCh-y4-Y1a">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Row 1 (initiate segue)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="nCW-ld-pIh">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="So6-9U-ErM" kind="show" id="z0W-ti-r34"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="row2" textLabel="3mJ-aS-ejI" style="IBUITableViewCellStyleDefault" id="MSe-Gh-vGZ">
<rect key="frame" x="0.0" y="72" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="MSe-Gh-vGZ" id="L8Q-kb-DQE">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Row 2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="3mJ-aS-ejI">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="row3" textLabel="8X7-0k-gW2" style="IBUITableViewCellStyleDefault" id="1hF-90-LNx">
<rect key="frame" x="0.0" y="116" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="1hF-90-LNx" id="aZt-ZZ-AdL">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Row 3" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="8X7-0k-gW2">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="aYp-Ls-x3G" id="0Tf-3x-ZFe"/>
<outlet property="delegate" destination="aYp-Ls-x3G" id="USF-qY-dKN"/>
</connections>
</tableView>
<navigationItem key="navigationItem" id="osl-fH-wOu"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="onQ-Z1-gxz" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1959" y="-210"/>
</scene>
<!--My Sub SubclassedTVC-->
<scene sceneID="npu-qz-94D">
<objects>
<tableViewController id="So6-9U-ErM" customClass="MySubSubclassedTVC" customModule="TestProject" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="V7k-LQ-pkk">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<sections>
<tableViewSection headerTitle="MySubSubclassedTVC" id="DC5-O3-Y4i">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="Kss-5J-a4B">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Kss-5J-a4B" id="Epw-KJ-VU7">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="So6-9U-ErM" id="0vY-8g-c6A"/>
<outlet property="delegate" destination="So6-9U-ErM" id="15t-qo-URt"/>
</connections>
</tableView>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Fi9-A8-mP0" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2942" y="-210"/>
</scene>
<!--My Tab BarVC-->
<scene sceneID="Ei6-s2-GoC">
<objects>
<tabBarController id="gUO-Uk-rVh" customClass="MyTabBarVC" customModule="TestProject" customModuleProvider="target" sceneMemberID="viewController">
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="NjD-wU-fg3">
<rect key="frame" x="0.0" y="0.0" width="375" height="49"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tabBar>
<connections>
<segue destination="6Lg-yF-lMB" kind="relationship" relationship="viewControllers" id="oCO-J8-zSr"/>
<segue destination="3ub-SL-PrQ" kind="relationship" relationship="viewControllers" id="Lgv-aG-XL7"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="UEB-ST-kDp" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-41" y="121"/>
</scene>
<!--Item-->
<scene sceneID="2Iq-t0-rlN">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="6Lg-yF-lMB" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Item" id="vKq-bx-53o"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="N9C-Do-5iD">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="aYp-Ls-x3G" kind="relationship" relationship="rootViewController" id="Ebo-G6-ePb"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bek-vp-oL7" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1030" y="-210"/>
</scene>
</scenes>
</document>
//
// MyApp.swift
// TestProject
//
// Created by Frank J. Edwards on 9/8/18.
// Copyright © 2018 Frank J. Edwards. All rights reserved.
//
/*
* From this thread: https://forums.developer.apple.com/message/329975
*
* Execute this code and you'll get a tab bar with two items, the first (and default)
* is a table view. Click on the first row to initiate a segue to a second table view.
* (This represents a user wanting to edit the data represented by the row.) When the
* user is done and clicks _Done_ in the navigation bar, the `viewWillDisappear()`
* method invokes a callback to pass data back to the parent. The parent should change
* the model and register an undo operation.
*
* You'll notice in the output that all pieces of code can see the undoManager object
* except the closure in the parent. (Look for "NOT found" in the output.)
*
* Why? Shouldn't the undoManager that was accessible before still be accessible?
* If this code is wrong, what is the right way to pass data back to the parent and
* have it register the undo operation?
*/
import UIKit
class MyTabBarVC: UITabBarController {
private var _undoManager = UndoManager()
override var undoManager: UndoManager { return _undoManager }
override var canBecomeFirstResponder: Bool { return true }
}
protocol DelegateForCallback {
func callbackFunction(_ msg: String)
}
class MyTableViewVC: UITableViewController, DelegateForCallback {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let typ = "MyTableViewVC"
if let _ = self.undoManager {
print("\(typ).viewDidAppear: Found in 'self'")
} else {
print("\(typ).viewDidAppear: NOT found in 'self'")
}
if let _ = view.undoManager {
print("\(typ).viewDidAppear: Found in 'view'")
} else {
print("\(typ).viewDidAppear: NOT found in 'view'")
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let vc = segue.destination as? MySubSubclassedTVC else {
fatalError()
}
vc.callbackClosure = {
self.callbackFunction("via closure")
}
vc.delegateForCallback = self
}
func callbackFunction(_ msg: String) -> Void {
let typ = "MyTableViewVC"
if let _ = self.undoManager {
print("\(typ).callbackFunction: Found in 'self' \(msg)")
} else {
print("\(typ).callbackFunction: NOT found in 'self' \(msg)")
}
if let _ = self.view.undoManager {
print("\(typ).callbackFunction: Found in 'self.view' \(msg)")
} else {
print("\(typ).callbackFunction: NOT found in 'self.view' \(msg)")
}
}
}
class MySubclassedTVC: MyTableViewVC {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let typ = "MySubclassedTVC"
if let _ = self.undoManager {
print("\(typ).viewDidAppear: Found in 'self'")
} else {
print("\(typ).viewDidAppear: NOT found in 'self'")
}
if let _ = view.undoManager {
print("\(typ).viewDidAppear: Found in 'view'")
} else {
print("\(typ).viewDidAppear: NOT found in 'view'")
}
}
}
class MySubSubclassedTVC: MySubclassedTVC {
var callbackClosure: (() -> Void)? = nil
var delegateForCallback: DelegateForCallback?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let typ = "MySubSubclassedTVC"
if let _ = self.undoManager {
print("\(typ).viewDidAppear: Found in 'self'")
} else {
print("\(typ).viewDidAppear: NOT found in 'self'")
}
if let _ = view.undoManager {
print("\(typ).viewDidAppear: Found in 'view'")
} else {
print("\(typ).viewDidAppear: NOT found in 'view'")
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
let typ = "MySubSubclassedTVC"
if let _ = self.undoManager {
print("\(typ).viewWillDisappear: Found in 'self'")
} else {
print("\(typ).viewWillDisappear: NOT found in 'self'")
}
if let _ = view.undoManager {
print("\(typ).viewWillDisappear: Found in 'view'")
} else {
print("\(typ).viewWillDisappear: NOT found in 'view'")
}
callbackClosure?()
delegateForCallback?.callbackFunction("via delegate")
}
}
@Azhrei
Copy link
Author

Azhrei commented Sep 11, 2018

Obviously, the Main-storyboard.xml has to be renamed to Main.storyboard, but GitHub wouldn't let me attach a file with extension of .storyboard

Images: Storyboard and Class Diagram

@Azhrei
Copy link
Author

Azhrei commented Sep 16, 2018

Run the above code in an Xcode project (I'm using Xcode 9.4.1 and targeting iOS 11.4 with Swift 4.1). I'm targeting an iPhone 8 Plus, but it doesn't matter which device is used.

When the main window appears, you'll see a table with three entries and four lines of text in the debug window. The text indicates whether undoManager has a valid value or whether it contains nil. So far, it has been non-nil in the places shown:

MyTableViewVC.viewDidAppear: Found in 'self'
MyTableViewVC.viewDidAppear: Found in 'view'
MySubclassedTVC.viewDidAppear: Found in 'self'
MySubclassedTVC.viewDidAppear: Found in 'view'

Click on the first row and the view replaces itself with a view pushed onto the nav controller's stack. The following appear in the debug window. So far, everything is good and undoManager is found in all cases:

MyTableViewVC.viewDidAppear: Found in 'self'
MyTableViewVC.viewDidAppear: Found in 'view'
MySubclassedTVC.viewDidAppear: Found in 'self'
MySubclassedTVC.viewDidAppear: Found in 'view'
MySubSubclassedTVC.viewDidAppear: Found in 'self'
MySubSubclassedTVC.viewDidAppear: Found in 'view'

Now click the <Back button. The detail view's viewWillDisappear() is invoking a closure provided by the parent view's prepare(0 method. This closure attempts to access the undoManager and it fails (returns nil). See the text below:

MySubSubclassedTVC.viewWillDisappear: Found in 'self'
MySubSubclassedTVC.viewWillDisappear: Found in 'view'
MyTableViewVC.callbackFunction: NOT found in 'self' via closure
MyTableViewVC.callbackFunction: NOT found in 'self.view' via closure
MyTableViewVC.callbackFunction: NOT found in 'self' via delegate
MyTableViewVC.callbackFunction: NOT found in 'self.view' via delegate
MyTableViewVC.viewDidAppear: Found in 'self'
MyTableViewVC.viewDidAppear: Found in 'view'
MySubclassedTVC.viewDidAppear: Found in 'self'
MySubclassedTVC.viewDidAppear: Found in 'view'

I don't understand why. The documentation for UIResponder indicates that the undoManager property does a dynamic lookup, walking up the hierarchy of views until it finds one. So why does the closure not have a valid undoManager property when the same class previous did have a valid one (see the MyTableViewVC.viewDidAppear() messages)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment