Open16

やり直しのiOS/macOSアプリ開発、その5

tana00tana00

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?
  }
}

SwiftUIにおける余白の適切な実装パターン - inSmartBank
)

tana00tana00

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)
  }
}

tana00tana00

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)
    }
  }
}
tana00tana00

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")
}
tana00tana00

初見で動作が解らんかったコード

  1. loader closureはclosureを引数にとるclosureである。
  2. loaderを呼び出す(実行する)にclosureを引数に与える。
  3. loader実行時には{prods in print(prods)}、つまりcompletion(products)が実行される。
  4. しかし、こんな機能使い道は?

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
tana00tana00

C言語ライブラリのリンク e.g. mysql-client

  1. xcodeprojフォルダと同階層にフォルダを作成しその中にlib, include(Headerファイル)をコピーする。
  2. Xcodeのプロジェクトを作成し、Project > Build Settings > All > User Header Search Pathsに$(PROJECT_DIR)/MySQL/include, (recursive)
  3. Target > Build Phases > Link Binary With Libraries, libmysqlclient.dylib, libc++.tbd
  4. 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()

tana00tana00

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()
tana00tana00

Publisherの実装例
Understanding Schedulers in Swift Combine Framework

receiveメソッドのシグネチャにギョッとするが、引数に型パラメータを含むジェネリクス関数なのだ。

pub.swift
// この実装の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

tana00tana00

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オプション付きでコンパイル

timer.swift
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
tana00tana00

Binding変数

次のSwiftUIコードの抜粋には状態変数isRedの出現箇所が2箇所ある。
2番めに$記号がついている訳がようやく判明した。

  1. Buttonのinitが受け取るaction closureがisRedを参照している
  2. Toggleのinitが受け取るBinding<Boot>型のisOn変数にisRedが渡っている
binding.swift
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.
tana00tana00

速習Objective-C

Fooクラスを宣言

foo.h
#import <Cocoa/Cocoa.h>
@interface Foo: NSObject
@property (nonatomic)int a;
- (void)foo;
@end

インスタンス変数とプロパティは区別される

foo.mm
#import "foo.h"
@implementation Foo
- (void)foo {
  NSLog(@"a = %d", a);
}
@synthesize a; // インスタンス変数(ivar, _a)とプロパティ(a)を合成
@end

Swiftから利用するにはBridging Headerが必要

bridge.h
#import "foo.h"

Fooクラスを呼出すSwiftファイル

main.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

tana00tana00

Stringをthrowするには?

extension String: CustomNSError {
  public var errorUserInfo: [String : Any] {  [NSLocalizedDescriptionKey: self ] }
 // or ["": self]
}
usage.swift
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

tana00tana00

型パラメータによる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は型パラメータの書き換えで行き来出来る。

SwiftUI×Firebaseでチャットアプリを作る(Swift Zoomin' #6) - YouTube

tana00tana00

Playgoundで動いていたAppをmacOS Projectへ移植するとトラブル発生

Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"

  1. 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."

  1. Target, Info, App Transport Security Settings, Allow Arbitray Loads, YES

ATS: データ転送防衛保証、https接続しないとAppStoreがAcceptしない

tana00tana00

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の値を使用できる。