Mobile App Dev 2022W: Tutorial 5
This tutorial is still being developed.
In this tutorial you will be playing with textanalyzer-3, which implements the same text analyzer functionality as Tutorial 2's textanalyzer-1 and [Mobile App Dev 2022W: Assignment 1|Assignment 1's textanalyzer-2]. This version, however, uses UIKit/Storyboard, not SwiftUI.
Getting Started
Unlike past tutorials, here we are textanalyzer-3 supplying the contents of the entire project rather than just one file. If you have problems with this project you'll need to re-create the project as follows:
- Create a new iOS (not multiplatform) App. The interface should be "Storyboard" (NOT SwiftUI) and the language should be Swift.
- Replace the contents of ViewController.swift, either by overwriting the file or copying and pasting the code below. (Copy from the raw source file, not this page.)
- Replace the contents of Main.storyboard, either by overwriting the file or by copying and pasting in the source view in Xcode by right clicking on "Main" in the Navigator pane and selecting Open As -> Source Code.
Key Concepts & APIs
While there are web pages on UIKit and Storyboard in Apple's main documentation websites, today the clear emphasis is on SwiftUI and it is much harder to find specific documentation on these older technologies. So I mostly relied on other sources.
Appcoda has a lot of valuable resources on iOS UIKit. I finally figured out how to do menus through this article by Gabriel Theodoropoulos on UIKit additions in iOS 14, and I found Simon Ng's introduction to Storyboard layouts very helpful.
Questions
- Why are we using a class (on line 10) rather than a struct?
- How are the @IBOutlet variables (lines 12-15) related to the contents of Main.storyboard? What about the @IBAction function (line 59)?
- What does updateAnalysis do? Why are there so many calls to it? What is the corresponding code in textanalyzer-2?
- What does setupAnalysisMenu() do? What is the corresponding code for this in textanalyzer-2?
- How do the Storyboard constraints determine the app's layout? Specifically, why is the menu at the top, the results at the bottom, and the text input in the center of the screen?
Tasks
- Make the title of the app "Text Analyzer 4" and make the text non-bold (while keeping the same font size).
- Put the results area in the center of the screen and the text input area just below it (thus swapping the position of the two). Make sure your layout works with different phone sizes and when the phone is rotated.
- Add the Empty and Pets Mentioned actions as they were in textanalyzer-2.
- Add a menu item "None" that sets the analysis mode to "None". When selected, the result area should show "Please choose an analysis mode." (This message should appear without you changing any part of the code except for the part determining the contents of the menu.)
textanalyzer-3 Code
ViewController.swift
//
// ViewController.swift
// textanalyzer-3
//
// Created by Anil Somayaji on 2/3/22.
//
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var t: UITextField!
@IBOutlet weak var analysisMenuButton: UIButton!
@IBOutlet weak var analysisResult: UILabel!
@IBOutlet weak var appTitle: UILabel!
var analysisMode = "None"
// override func viewDidLoad() {
// super.viewDidLoad()
// }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setupAnalysisMenu()
updateAnalysis()
}
func setupAnalysisMenu() {
var menuItems: [UIAction] = []
for mode in analysis.keys {
let item = UIAction(title: mode) {
(action) in
self.analysisMode = mode
self.updateAnalysis()
}
menuItems.append(item)
}
let analysisMenu = UIMenu(title: "", children: menuItems)
analysisMenuButton.showsMenuAsPrimaryAction = true
analysisMenuButton.menu = analysisMenu
}
func updateAnalysis() {
if let inputText = t.text {
if let analysisFunc = analysis[analysisMode] {
let r = analysisFunc(inputText)
analysisResult.text = analysisMode + ": " + r
} else {
analysisResult.text = "Please choose an analysis mode."
}
}
}
@IBAction func doAnalysis(_ sender: Any) {
updateAnalysis()
}
}
func countUpper(s: String) -> String {
var count = 0
let upperCase: Set<Character> =
["A","B","C","D","E","F","G","H","I","J","K","L","M",
"N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
for c in s {
if (upperCase.contains(c)) {
count += 1
}
}
return String(count)
}
let analysis: [String: (String) -> String] = [
"Count": countCharacters,
"Upper Case": countUpper
]
func countCharacters(s: String) -> String {
let charcount = s.count
return String(charcount)
}
Main.storyboard
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="textanalyzer_3" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="428" height="926"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Text Analyzer" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZtZ-ps-7Ye">
<rect key="frame" x="107.33333333333333" y="130" width="213.33333333333337" height="41"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Result Area" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1Vv-DZ-Mly">
<rect key="frame" x="170.66666666666666" y="498" width="86.999999999999972" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Enter Text" textAlignment="center" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="lai-Vo-ERj">
<rect key="frame" x="167.66666666666666" y="446" width="92.999999999999972" height="34"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits"/>
<connections>
<action selector="doAnalysis:" destination="BYZ-38-t0r" eventType="editingChanged" id="nO1-M0-6Er"/>
</connections>
</textField>
<button opaque="NO" contentMode="scaleToFill" ambiguous="YES" showsMenuAsPrimaryAction="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="OZw-vK-PXJ">
<rect key="frame" x="164.66666666666666" y="46" width="98.999999999999972" height="30"/>
<state key="normal" title="Analysis Menu"/>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="OZw-vK-PXJ" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="5v2-LY-EP2"/>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="1Vv-DZ-Mly" secondAttribute="bottom" constant="10" id="Qa6-s1-ne8"/>
<constraint firstItem="ZtZ-ps-7Ye" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="UGF-av-neF"/>
<constraint firstItem="OZw-vK-PXJ" firstAttribute="top" relation="lessThanOrEqual" secondItem="6Tk-OE-BBY" secondAttribute="top" constant="10" id="XYA-0a-ubh"/>
<constraint firstItem="ZtZ-ps-7Ye" firstAttribute="top" relation="greaterThanOrEqual" secondItem="OZw-vK-PXJ" secondAttribute="bottom" constant="10" id="cFf-s5-fgz"/>
<constraint firstItem="1Vv-DZ-Mly" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="psz-eR-tP5"/>
<constraint firstItem="lai-Vo-ERj" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="rod-O1-UBb"/>
<constraint firstItem="lai-Vo-ERj" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="z38-IR-hwi"/>
</constraints>
</view>
<connections>
<outlet property="analysisMenuButton" destination="OZw-vK-PXJ" id="1Ec-3D-09d"/>
<outlet property="analysisResult" destination="1Vv-DZ-Mly" id="rO4-z7-K7C"/>
<outlet property="appTitle" destination="ZtZ-ps-7Ye" id="xVP-yj-fvd"/>
<outlet property="t" destination="lai-Vo-ERj" id="AkQ-xR-U8T"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="29.600000000000001" y="83.208395802098963"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>