やり直しのiOS/macOSアプリ開発、その5
Three means of View layout variation
以下のVStack3つは同じ見た目を実現出来る。3番めが変更に強いそうだ。
struct ContentView: View {
var body: some View {
// 1
VStack(spacing: 0) {
Text("First")
.padding(.bottom, 16)
Text("Second")
.padding(.bottom, 8)
Text("Third")
}.border(.yellow)
// 2
VStack(spacing: 0) {
Text("First")
Spacer().frame(height: 16)
Text("Second")
Spacer().frame(height: 8)
Text("Third")
}
.border(.yellow)
// 3
VStack(spacing: 16) {
Text("First")
VStack(spacing: 8) {
Text("Second")
Text("Third")
}
}
.border(.cyan)
Text("The Another")
// 1. padding, 2. Spacer().frame(height:), 3. spacing:
// Which is best?
}
}
Table Cellの例
struct ContentView: View {
let userName: String
init(useName: String = "User 0") {
self.userName = useName
}
var body: some View {
HStack(alignment: .top, spacing: 8) {
Image(systemName: "person.circle.fill")
.foregroundColor(.gray)
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
Text(userName)
.font(.headline)
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: "ellipsis")
.foregroundColor(.gray)
}
Text("Hello. My name is \(userName). I am looking forward to getting to know you all.")
.font(.subheadline)
.foregroundColor(.gray)
.multilineTextAlignment(.leading)
}
}
.padding(16)
.background(Color(.windowFrameTextColor))
.cornerRadius(12)
.border(.yellow)
}
}
anchor: UnitPoint(x:y:), x,y: 0...1
anchorとは回転させるViewの回転中心のこと。左上隅が原点でScreen座標系の値を指定する。Viewの重心を中心に回転させるなら、0.5, 0.5を与える。
struct ContentView: View {
var body: some View {
VStack {
// 1
ZStack {
Text("First")
.padding(.bottom, 16)
.padding(.top, 16)
Text("Second")
.rotationEffect(Angle(degrees: 30), anchor: UnitPoint(x: 0.0, y: 0.0))
}
.font(.system(size: 30))
.border(.yellow)
// 2
ZStack {
Text("First")
.padding(.bottom, 16)
.padding(.top, 16)
Text("Second")
.rotationEffect(Angle(degrees: 30), anchor: UnitPoint(x: 0.5, y: 0.0))
}
.font(.system(size: 30))
.border(.yellow)
}
}
}
3回失敗するまでトライする、、ってどうやって記述?
let url = NSURL(string: "https://finance.yahoo.co.jp/")
var html = ""
var success = false
var failureCount = 0
while !success && failureCount < 3 {
do {
html = try String(contentsOf: url! as URL)
success = true
} catch {
sleep(3)
failureCount += 1
}
}
if !success {
print("failure")
}
初見で動作が解らんかったコード
- loader closureはclosureを引数にとるclosureである。
- loaderを呼び出す(実行する)にclosureを引数に与える。
- loader実行時には{prods in print(prods)}、つまりcompletion(products)が実行される。
- しかし、こんな機能使い道は?
Closures vs Protocols for passing data between modules | by mohammad abdalraziq | Jun, 2023 | Medium
typealias ProductsLoader = (([Product])->Void)->Void //Closure taking Closure
var loader: ProductsLoader = {completion in // initを呼ぶにはclosure引数
// we can load products here.
let products =
[
Product(name: "Product1", price: 100),
Product(name: "Product2", price: 200),
Product(name: "Product3", price: 300)
]
completion(products)
}
loader {prods in print(prods)} // = completion: [Product] -> Void
Swift Macro, Compile option
---
C言語ライブラリのリンク e.g. mysql-client
- xcodeprojフォルダと同階層にフォルダを作成しその中にlib, include(Headerファイル)をコピーする。
- Xcodeのプロジェクトを作成し、Project > Build Settings > All > User Header Search Pathsに$(PROJECT_DIR)/MySQL/include, (recursive)
- Target > Build Phases > Link Binary With Libraries, libmysqlclient.dylib, libc++.tbd
- Add File > C++ File > Bridging-Header.h
#import <mysql.h>
#import <errmsg.h>
MOSA Multi-OS Software Artists » 【MOSAメルマガ#25】SwiftでMySQLに接続する (2018年3月6日配信)
MySQL :: MySQL 8.0 C API Developer Guide :: 5.4.58 mysql_real_connect()
OperationQueue, Threadを使った並列処理
OperationQueue
import Foundation
func doTask(number: Int) {
print("Task \(number) started.")
Thread.sleep(forTimeInterval: 1) // なんかの処理をシミュレート
print("Task \(number) finished.")
}
func main() {
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 2 // 最大同時実行数を設定
for i in 1...5 {
let operation = BlockOperation {
doTask(number: i)
}
operationQueue.addOperation(operation)
}
operationQueue.waitUntilAllOperationsAreFinished() // すべてのタスクが終了するまで待つ
print("All tasks are finished.")
}
main()
Thread
import Foundation
func doTask(number: Int) {
print("Task \(number) started.")
Thread.sleep(forTimeInterval: 1) // なんかの処理をシミュレート
print("Task \(number) finished.")
}
func main() {
for i in 1...5 {
let thread = Thread {
doTask(number: i)
}
thread.start()
}
Thread.sleep(forTimeInterval: 2) // スレッドの終了を待つために少し待つ
print("All tasks are finished.")
}
main()
DispatchGroupを使った処理の待ち合わせ。コードには現れないQueueを使って待ち合わせを行うので高効率。
import Foundation
func doTask(number: Int, group: DispatchGroup) {
print("Task \(number) started.")
Thread.sleep(forTimeInterval: TimeInterval(arc4random_uniform(5) + 1)) // ランダムな時間処理をシミュレート
print("Task \(number) finished.")
group.leave() // タスクの終了を通知
}
func main() {
let group = DispatchGroup()
for i in 1...5 {
group.enter() // タスクの開始を通知
DispatchQueue.global().async {
doTask(number: i, group: group)
}
}
_ = group.wait(timeout: .distantFuture) // すべてのタスクが終了するまで待つ
print("All tasks are finished.")
}
main()
並列処理にThreadが使われない理由。Threadの待ち合わせに高コスト処理が必要。
import Foundation
func doTask(number: Int) {
print("Task \(number) started.")
Thread.sleep(forTimeInterval: 1) // なんかの処理をシミュレート
print("Task \(number) finished.")
}
func main() {
var ar: [Thread] = []
for i in 1...5 {
let thread = Thread {
doTask(number: i)
}
ar.append(thread)
thread.start()
}
// wait for threads using ar.
for thread in ar {
while thread.isExecuting { // 高コスト
// Wait until the thread finishes executing
}
}
print("All tasks are finished.")
}
main()
Understanding Schedulers in Swift Combine Framework
Publisherの実装例receiveメソッドのシグネチャにギョッとするが、引数に型パラメータを含むジェネリクス関数なのだ。
// この実装のPublisherのsinkメソッドを呼べば即座にイベントが流れる。
// イベントが発生するのを待つのであれば、subscriber.receive(event or control)メソッドはCustom Subscriptionの中で呼ばれるべき。
import Combine
import Foundation
struct BusyPublisher: Publisher {
typealias Output = Int
typealias Failure = Never
func receive<S>(subscriber: S) where S : Subscriber,
Failure == S.Failure, Output == S.Input {
sleep(2)
subscriber.receive(subscription: Subscriptions.empty)
_ = subscriber.receive(100)
_ = subscriber.receive(200)
subscriber.receive(completion: .finished)
}
}
let p = BusyPublisher()
let _ = p.sink { e in print("Received value: \(e)") } // => 100, 200
// p.send(10)// Publisher has no send method. Actually, Subject has the one.
print("Hello")
Combine — Creating a custom subscriber | by Jullian Mercier | Medium
Custom Subscriptionの実装意図が良くわからん?
Subscriber: イベントの処理
Publisher: イベントの生成
Subscription: 生成したイベントのハンドル(実際にはSubscriberへ送るだけ)
【Swift】Custom Publisherを作成してCombineフレームワークの動きを学ぶ - Qiita
lldbでプロセス実行時にThreadを確認
breakを打つにはbr set -f timer.swift -l 10
とする。list timer.swift 1
でソース表示
>lldb timer
(lldb) target create "timer"
Current executable set to '/Users/tanaka/Documents/junk/swift/timer' (arm64).
(lldb) br set -f timer.swift -l 10
Breakpoint 1: where = timer`main + 340 at timer.swift:10:1, address = 0x0000000100003530
(lldb) r
Process 38240 launched: '/Users/tanaka/Documents/junk/swift/timer' (arm64)
Outside Task
Process 38240 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100003530 timer`main at timer.swift:10:1
7 // 2
8 print("Outside Task")
9 // 3
-> 10 dispatchMain() // parks the main thead
11
12 // 並行処理の例(並列ではない)
13
Target 0: (timer) stopped.
(lldb) th list
Process 38240 stopped
* thread #1: tid = 0x1bbc002, 0x0000000100003530 timer`main at timer.swift:10:1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
thread #2: tid = 0x1bbc05f, 0x000000018a60ff54 libsystem_kernel.dylib`mach_msg2_trap + 8, queue = 'com.apple.root.user-initiated-qos.cooperative'
(lldb)
-g
オプション付きでコンパイル
import Dispatch
Task {
try! await Task.sleep(nanoseconds: 1_000_000_000)
// 1
print("Inside Task")
}
// 2
print("Outside Task")
// 3
dispatchMain() // parks the main thead
Binding変数
次のSwiftUIコードの抜粋には状態変数isRedの出現箇所が2箇所ある。
2番めに$記号がついている訳がようやく判明した。
- Buttonのinitが受け取るaction closureがisRedを参照している
- Toggleのinitが受け取るBinding<Boot>型のisOn変数にisRedが渡っている
HStack {
// 1
Button(isRed ? "Blue" : "Red") { isRed.toggle() }
Spacer()
// 2
Toggle("Change Color", isOn: $isRed)
}
状態変数をaction closure以外で変更すると実行時に警告が出る
Modifying state during view update, this will cause undefined behavior.
速習Objective-C
Fooクラスを宣言
#import <Cocoa/Cocoa.h>
@interface Foo: NSObject
@property (nonatomic)int a;
- (void)foo;
@end
インスタンス変数とプロパティは区別される
#import "foo.h"
@implementation Foo
- (void)foo {
NSLog(@"a = %d", a);
}
@synthesize a; // インスタンス変数(ivar, _a)とプロパティ(a)を合成
@end
Swiftから利用するにはBridging Headerが必要
#import "foo.h"
Fooクラスを呼出すSwiftファイル
import Foundation
var a = Foo()
a.a = 100
a.foo()
コンパイルするには特殊オプションが必要
>clang -c foo.mm
>swiftc -c main.swift -import-objc-header bridge.h #コンパイル
>swiftc -o app main.o foo.o #リンク
>./app
2023-09-14 19:33:12.462 app[47975:53516185] a = 100
実はBridging Headerの代わりにfoo.h
を使っても同じ結果が得られる。
>swiftc -c main.swift -import-objc-header foo.h #コンパイル
>swiftc -o app main.o foo.o #リンク
>./app
ターミナルを使ってSwiftからObjective-Cを利用する
note: MultitouchSupport.tbd
ファイルとはtext based definitionファイル。frameworkのシンボルに関する情報が記載。e.g. MTDeviceGetGUID
Stringをthrowするには?
extension String: CustomNSError {
public var errorUserInfo: [String : Any] { [NSLocalizedDescriptionKey: self ] }
// or ["": self]
}
var a: Int = 0
func next() -> Result<Int, Error> {
// func next() -> Result<Int, MyErr> {
let c: () -> Int = {
a += 1
return a
}
let b = c()
if b > 3 {
return Result.failure("err!!")
// return Result.failure("err" as! Error)
} else {
return Result.success(b)
}
}
do {
print(try next().get())
print(try next().get())
print(try next().get())
print(try next().get())
} catch {
print(error) // => err!!
}
ChatGPTSwiftUI/Shared/ChatGPTAPI.swift at main · alfianlosari/ChatGPTSwiftUI · GitHub
型パラメータによるDI
protocol DB {
static func foo() // 1.
}
class DummyDB: DB { // 2-1.
static func foo() { print("foo") }
}
class ProductionDB: DB { // 2-2
static func foo() { print("bar") }
}
class UseDB<Database: DB> { // 3
var db: () { Database.foo()}
}
// DI using Type Parameter
UseDB<DummyDB>().db // => foo
UseDB<ProductionDB>().db // => bar
// dbインスタンスを2つ試したい。DummyDB, ProductionDBの2つはstaticメソッドを持ってる場合、その2つのいずれかをインスタンス化したdbは型パラメータの書き換えで行き来出来る。
Playgoundで動いていたAppをmacOS Projectへ移植するとトラブル発生
Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"
- Target, Signing & Capabilities, App Sandbox, Outgoing Connectins(Client)
Error Domain=NSURLErrorDomain Code=-1022 "The resource could not be loaded because the App Transport Security policy requires the use of a secure connection."
- Target, Info, App Transport Security Settings, Allow Arbitray Loads, YES
ATS: データ転送防衛保証、https接続しないとAppStoreがAcceptしない
property initializers vs class initializer
class A {
var ar: [Int] = []
lazy var isEmpty: Bool = ar.isEmpty
}
// error: cannot use instance member 'ar' within property initializer; property initializers run before 'self' is available
let a = A()
print(a.isEmpty)
class B {
var ar: [Int] = []
var isEmpty: Bool
init() {
print(ar)
isEmpty = ar.isEmpty
}
}
let b = B()
print(b.isEmpty)
// インスタンス生成の手順
// 1. クラスがメモリにload
// 2. property initializer, この際propertyの値は使用出来ない
// 3. propertyはinstanciateの際に初期化されても良い
class C {
var ar: [Int] = []
lazy var isEmpty: Bool = ar.isEmpty
init() {
print(ar)
}
}
let c = C()
print(c.isEmpty)
// 1. lazy property initializerはpropertyの値を使用できる。