THERAMPAGE
THERAMPAGE
THERAMPAGE
Switch to the Russian version
Main   |   Blog   |   EngRead   |   Dragon: The Eater   |   Rampage CMS

Non-blocking presentation of view controllers

Sometimes a situation arises in an application when several parallel processes come to the conclusion that each of them needs to display some kind of view controller. For example, both processes asynchronously request some data from the server. One is the version of the application, the second is information to display to the user. If there is a new version, the first process immediately displays a dialog box asking you to upgrade. The second process opens a window with information when data is received.
import UIKit
import PlaygroundSupport
class Controller: UIViewController {
    override func loadView() {
        view = UIView()
        view.backgroundColor = .red
    }
    override func viewDidLoad() {
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            // request a web-server for a latest app version
            DispatchQueue.main.async {
                self.showAlert()
            }
        }
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            // request a web-server for some data
            DispatchQueue.main.async {
                self.showScreen()
            }
        }
    }
    private func showAlert() {
        let alert = UIAlertController(
            title: "New version available",
            message: nil,
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "Close", style: .destructive))
        present(alert, animated: true)
    }
    private func showScreen() {
        let controller = UIViewController()
        controller.view.backgroundColor = .green
        present(controller, animated: true)
    }
}
PlaygroundPage.current.liveView = Controller()
By running this code, you can easily make sure that the message about the new version is always displayed and the application never switches to another screen. This is due to the fact that the system simply ignores the second and all subsequent requests to display other view controllers if one is already displayed. The fact is that UIViewController has a property presentedViewController where the first and only possible presented view controller is written and stored there until it is released. What can be done in such a situation. Well, for example, wait for the open view controller to complete and then display the next one.
extension UIViewController {
    func safePresent(_ block: (() -> Void)?) {
        let noPresented  = nil == presentedViewController
        let noTransition = nil == transitionCoordinator
        noPresented && noTransition
            ? block?()
            : DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                [weak self] in self?.safePresent(block)
            }
    }
}
Usage example:
safePresent { [weak self] in
    self?.present(controller, animated: true)
}

The "if case .some" problem in Swift

As it known, support for optional types in Swift is implemented through an enum type called Optional. So when we write:
let a: Int? = 1 
this is syntactic sugar, the full notation is:
let a: Optional<Int> = .some(1) 
In theory, everyone knows this, and the language hides the implementation details from us well. But one day you might consider having the data type in your program like this:
enum Progress {
    case none
    case some
}
For example, you want to track some process in your app. You initialize the variable to .none and then once there is any progress, the value is changed to .some. Quite elegant:
import Foundation
enum Progress {
    case none
    case some(Float)
}
class Processing {
    var loadingProgress: Progress?
    init(_ progress: Progress) {
        loadingProgress = progress
    }
    func start() {
        loadingProgress = .some(1)
    }
    func check() {
        if case .some(_) = loadingProgress {
            print("The loading process have some progress")
        }
    }
}
let p = Processing(.none)
p.start()
p.check() 
On startup, we get the expected message:
The loading process have some progress
It seems to work well, but all of a sudden you notice strange behavior - some conditions don't work correctly. What's the matter? Let's run the check without immediately calling the start() method:
let p = Processing(.none)
p.check()
Oops. We got The loading process have some progress again message.
But there was no progress. To verify this display the value of the loadingProgress variable:
let p = Processing(.none)
p.check()
print("The loading process have (p.loadingProgress!) progress")
The result will be two lines:
The loading process have some progress
The loading process have none progress
 
You probably already guessed what the matter is here, since I started with a description of optional types. But without a hint, it might not be so obvious. Of course, the fact is that the loadingProgress variable is Optional but it compares with the .some value of the Progress type, so the condition is met. If we change the .some value in our Progress type to .done for example, then the problem disappears. You can complain that the code is not quite optimal, but its purpose is to demonstrate the problem. In a large project, something like this can slip in and cause a lot of problems. Moreover, if we try to perform the same trick with the value .none, then nothing will come of it.
if case .none = loadingProgress {
	print("The loading process have no progress")
}
Xcode will issue a warning:
Assuming you mean 'Optional<Progress>.none'; did you mean 'Progress.none' instead?
While .some will execute without any warning.
 
¯\_(ツ)_/¯

Directory of the iOS application running in the simulator

Sometimes it happens that you need to see what is in the iOS application directory. What files are stored, how much space they take, etc. But how to do this - it is not quite obvious.
 
The first and perhaps easiest thing is to add the following line somewhere in the application code: 
print("app folder path is (NSHomeDirectory())")
And then look in the console for the result of the execution. The option is certainly not bad, but rather cumbersome: you have to add the line, then find the output, copy the path, open the directory...
 
Another way is to use the following command: 
xcrun simctl get_app_container booted com.example.app data
As a result, the path to the directory we need will be displayed on the screen. This is also not the easiest command, but this can be improved, which we will do a little later, but for now we will consider the third option:
 
An app called RocketSim for Xcode Simulator [https://apps.apple.com/app/apple-store/id1504940162] A fantastic development tool that adds all kinds of helper features to the iOS simulator, including the ability to quickly navigate to the application directory. Highly recommended, but there is a caveat, it is only available for MacOS 12.4 and higher.
 
But let's go back to the previous command and see how we can improve it. At first, put it in a shell script, then add a convenient way to set the Bundle Id, process the results and display them or immediately open the directory we need.
 
#!/bin/sh
DEVICE_IDS="$(xcrun simctl list | grep Booted | awk -F '[()]' '{print $2}')"
while read -r line; do DEVICE_IDS_ARRAY+=("$line"); done <<<"$DEVICE_IDS"
DEVICE_NUMBER="${#DEVICE_IDS_ARRAY[@]}"
if [ "$DEVICE_NUMBER" = 1 ]; then
    DEVICE_ID=$DEVICE_IDS
else
    DEVICE_NAMES="$(xcrun simctl list | grep Booted | awk -F '[()]' '{print $1}')"
    while read -r line; do DEVICE_NAMES_ARRAY+=("$line"); done <<<"$DEVICE_NAMES"
    i=0
    for element in "${DEVICE_NAMES_ARRAY[@]}"
    do
        i=$((i+1))
        echo "$i) $element"
    done
    read -p "Choose which one: " DEVICE_POSITION
    DEVICE_ID=${DEVICE_IDS_ARRAY[DEVICE_POSITION-1]}
fi
echo "DEVICE_ID: ${DEVICE_ID}"
if [ -z "${DEVICE_ID}" ]; then
    echo "No running simulators have been found"
    exit 1
fi
getopts ":fFtT" CHOICE;
shift $((OPTIND - 1))
APP_NAME=$1
if [ -z "${APP_NAME}" ]; then
    read -p "Enter some part of an app budle id: " APP_NAME
fi
echo "APP_NAME: ${APP_NAME}"
APP_PATH=~/Library/Developer/CoreSimulator/Devices/$DEVICE_ID/data/Containers/Data/Application
echo "APP_PATH: ${APP_PATH}"
cd $APP_PATH
APP_ID=$(find . -iname *$APP_NAME* | head -n 1 | awk -F '/' '{print $2}')
echo "APP_ID: ${APP_ID}"
if [ -z "${APP_ID}" ]; then
    echo "Unable to find an app which bundle id contains '$APP_NAME'"
    exit 1
fi
echo "The path is:\n\n$APP_PATH/$APP_ID\n"
if [ -z "${CHOICE}" ] || [ "$CHOICE" = "?" ]; then
    read -p "Open [T]erminal or [F]inder window?: " CHOICE
fi
if [ "$CHOICE" = "T" ] || [ "$CHOICE" = "t" ]; then
    open -a Terminal "$APP_PATH/$APP_ID"
fi
if [ "$CHOICE" = "F" ] || [ "$CHOICE" = "f" ]; then
    open "$APP_PATH/$APP_ID"
fi
 
Одна страница