Неблокирующая презентация вью-контроллеров
Иногда в приложении возникает ситуация, когда несколько параллельных процессов приходят к тому, что каждому из них нужно отобразить какой-то вью-контроллер. Например, оба процесса асинхронно запрашивают с сервера какие-то данные. Один - версию приложения, второй - информацию для показа пользователю. Если есть новая версия, то первый процесс незамедлительно показывает диалоговое окно с предложением обновиться. Второй процесс открывает окно с информацией когда получены данные.
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()
Запустив этот код можно легко убедиться, что всегда отображается сообщение о новой версии и никогда не выполняется переход на другой экран. Это происходит из-за того, что система просто игнорирует второй и все последующие запросы на отображение других вью-контроллеров, если какой-то один уже отображается. Дело в том, что UIViewController есть свойство presentedViewController куда записывается первый и единственно возможный в момент времени презентованный вью-контроллер и хранится там пока не будет освобожден. Что можно сделать в такой ситуации. Ну например дождаться завершения открытого вью-контроллера и затем уже отобразить следующий.
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)
}
}
}
Пример использования:
safePresent { [weak self] in
self?.present(controller, animated: true)
}
Проблема “if case .some” в Swift
Как известно, поддержка опциональных типов в Swift реализована через тип enum под названием Optional. И когда мы пишем:
let a: Int? = 1
это синтаксический сахар, а полная запись:
let a: Optional<Int> = .some(1)
По идее, все это знают, а язык хорошо прячет от нас детали реализации и однажды в голову вполне может придти идея завести в свой программе вот такой тип данных:
enum Progress {
case none
case some
}
Например, вам нужно отслеживать какой-то процесс. Вы инициализируете переменную значением .none, а затем, если есть какой-то прогресс, то значение меняется на .some. Лаконично и изящно:
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()
При запуске мы получим вполне ожидаемое сообщение:
The loading process have some progress
Отлично, все работает. Но внезапно вы замечаете странное поведение - некоторые условия работают неверно. В чем же дело? Давайте запустим без сразу проверку без вызова метода start():
let p = Processing(.none) p.check()
Упс. Мы снова получили
The loading process have some progress
Но ведь никакого прогресса не было. Давайте убедимся в этом и выведем значение переменной loadingProgress:
let p = Processing(.none)
p.check()
print("The loading process have (p.loadingProgress!) progress")
Результатом будут две строки:
The loading process have some progress
The loading process have none progress
Вы наверное уже догадались в чем тут дело, так как я начал именно с описания работы опциональных типов. А вот без подсказки возможно было бы не так очевидно. Конечно дело в том, что переменная loadingProgress является Optional и именно с этим типом сравнивается значение .some и условие выполняется. Если мы изменим в нашем типе Progress значение .some например на .done, то проблема исчезнет. Можно придраться, что код не вполне оптимальный, но его целью является демонстрация проблемы. В большом проекте что-то такое вполне может проскользнуть и доставить массу проблем.
Более того, если мы попробуем провернуть тот же фокус со значением .none, то у нас ничего не выйдет:
if case .none = loadingProgress {
print("The loading process have no progress")
}
Xcode выдаст предупреждение:
Assuming you mean 'Optional<Progress>.none'; did you mean 'Progress.none' instead?
В то время как .some выполниться без каких-либо предупреждений.
¯\_(ツ)_/¯
Директория iOS приложения в запущенного в симуляторе
Бывает, что нужно посмотреть, как там дела у приложения на диске.
Какие файлы хранятся, сколько места занимают и т.д.
И как это сделать, не так чтобы очевидно.
Первый и самый простой способ это, пожалуй, добавить куда-нибудь в приложение строку:
print("app folder path is (NSHomeDirectory())")
И затем по необходимости искать в консоли результат ее выполнения.
Вариант конечно неплохой, но довольно громоздкий: прописать, найти, скопировать, вставить...
Другой способ, это воспользоваться командой:
xcrun simctl get_app_container booted com.example.app data
В результате на экран будет выведен путь до нужной нам директории.
Тут тоже не самая простая команда, но это можно доработать, чем мы немного позже и займемся, а пока рассмотрим третий вариант:
Приложение RocketSim for Xcode Simulator [https://apps.apple.com/app/apple-store/id1504940162]
Фантастическая вещь для разработчика, добавляющая к iOS симулятору всевозможные вспомогательные функции, в том числе и возможность быстро перейти в директорию приложения.
Крайне рекомендую, но есть нюанс, только для MacOS 12.4 и выше.
Но вернемся к нашей команде, как можно ее улучшить.
Ну для начала поместить ее в шелл скрипт.
Затем добавить удобную возможность задавать Bundle Id,
обработать полученные результаты в вывести их в удобной форме, например сразу открыв нужно нам окно. Вуаля и вот результат:
#!/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
Одна страница