[Swift] データの受け渡し: @ObservedObject

class CounterModel: ObservableObject {
    @Published var count: Int = 0
}

struct CounterPage: View {
    @ObservedObject var counter = CounterModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("カウンター")
                .font(.title)
            
            Text("現在の値: \(counter.count)")
                .font(.headline)
                 
            Button("+1") {
                counter.count += 1
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
                 
            Button("リセット") {
                counter.count = 0
            }
            .padding()
            .background(Color.red)
            .foregroundColor(.white)
            .cornerRadius(8)
            
            Spacer()
        }
        .padding()
    }
}

使い方: 小画面に渡す

NavigationLink(destination: CounterPage(counter: counter)) {
    Text("カウンターページへ")
}

小画面で受け取って使う

struct CounterPage: View {
    @ObservedObject var counter: CounterModel

    var body: some View {
        VStack {
            Text("現在の値: \(counter.count)")
            Button("+1") { counter.count += 1 }
        }
    }
}

ObservableObject クラスは「共有したいデータの所有者側」で定義・生成」 するのが基本

[Swift] データの受け渡し :bindingと値渡し

ContentView.swift

                NavigationLink(destination: CounterPage(message: userMessage)) {
                    Text("カウンターページへ")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.green)
                        .cornerRadius(8)
                }

CounterPage.swift

struct CounterPage: View {
    let message: String
    @State private var count = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("受け取ったメッセージ:")
                        Text(message)   // ← 表示して確認
                            .font(.title)
                            .foregroundColor(.blue)
 // 省略

ContentView.swiftで、api側にデータを送る際には、 TextField(“メッセージを入力”, text: $userMessage) としているのに、今回は NavigationLink(destination: CounterPage(message: userMessage)) としている

「text: $userMessage」と「message: userMessage」の違いは バインディング(Binding)か値のコピーか

1. text: $userMessage
$ をつけると バインディング(Binding) を渡している。
バインディングは「元の変数と直接つながっている参照」のようなもの。
TextField の入力が変わると、自動的に @State var userMessage の値も更新される。

👉 双方向のデータやり取りが可能。
例:入力欄に文字を打つと userMessage が変わるし、逆に userMessage を変えても入力欄が変わる。

2. message: userMessage
$ がついていないので、ただの 値のコピー を渡している。
遷移先 CounterPage の message: String に「現在の値」を渡すだけ。
遷移先で message を書き換えても、元の userMessage には影響しない。

元の画面で変更内容を反映させたい時などはbindingの方が良い

iOSのstate

stateとは、アプリのローカルメモリ
アプリを終了すると消える仕組み、アプリごとにローカルメモリを持つ

iOSアプリは1つの プロセス として動作する

OSは各プロセスに 独立した仮想アドレス空間 を割り当てる
アプリA (プロセスA) → 仮想メモリ 0x0000_0000〜0xFFFF_FFFF
アプリB (プロセスB) → 仮想メモリ 0x0000_0000〜0xFFFF_FFFF

iOSはメモリ管理に 制約が強い
バックグラウンドアプリは一定時間で メモリを解放される
メモリ不足になるとアプリが強制終了されることもある

iOSも Linux と同じく プロセスの中にスレッドがある 仕組みになっています。正確には、iOS は Darwin(macOS/iOSのカーネル)上で動く Unix 系 OS なので、プロセスとスレッドの概念は Linux とほぼ同じ

DispatchQueue.global(qos: .background).async {
    // 重い処理
    let result = doHeavyTask()

    // UI更新はメインスレッドで
    DispatchQueue.main.async {
        self.aiReply = result
    }
}
Task {
    let result = await fetchDataFromServer()
    // メインスレッドでUI更新
    await MainActor.run {
        self.aiReply = result
    }
}
let queue = OperationQueue()
queue.addOperation {
    let result = doHeavyTask()
    OperationQueue.main.addOperation {
        self.aiReply = result
    }
}

### 実際にstateを実装してみる
CounterPage.swift

import SwiftUI

struct CounterPage: View {
    @State private var count = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("カウンター")
                .font(.title)
            
            Text("現在の値: \(count)")
                .font(.headline)
                 
            Button("+1") {
                count += 1
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
                 
            Button("リセット") {
                count = 0
            }
            .padding()
            .background(Color.red)
            .foregroundColor(.white)
            .cornerRadius(8)
            
            Spacer()
        }
        .padding()
    }
}

ユーザー操作やイベントによって変わる値を保持することが多い
なるほど〜

[Swift] 非同期処理の並行処理

async / awaitの組み合わせで非同期処理を行う

ContentView.swift

func getWho() async -> String {
    await Task.sleep(5 * 1000 * 1000 * 1000)
    return "山本さん"
}

func getMessage() async -> String {
    await Task.sleep(3 * 1000 * 1000 * 1000)
    return "ハロー"
}

class StopWatch: ObservableObject {
    
    @Published var elapsedTime: Double = 0.0
    private var timer = Timer()
    
    func start() {
        guard !timer.isValid else { return }
        self.elapsedTime = 0.0
        timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) {
            _ in
            self.elapsedTime += 0.01
        }
    }
    
    func stop() {
        timer.invalidate()
    }
}

struct ContentView: View {
    
    @State var message:String = ""
    @ObservedObject var watch = StopWatch()
    
    var body: some View {
        VStack {
            Button(action: {
                Task {
                    watch.start()
                    message = "_ _ _"
                    
                    async let who = getWho()
                    async let msg = getMessage()
                    message = await who + "," + msg + " ! "
                    watch.stop()
                }
            }) {
                Label("async TEST", systemImage: "testtube.2")
                    .background(
                        Capsule()
                            .stroke(lineWidth: 1)
                            .frame(width: 180, height: 40)
                    )
            }.padding(30)
            Text("\(message)").font(.title2)
            let milliSeconds = Int((watch.elapsedTime) * 100)%100
            let seconds = Int(watch.elapsedTime)
            Text("\(seconds).\(milliSeconds)").padding()
            Spacer()
        }
    }
}

OK, もう一声

[Swift] 画像データを非同期処理で表示

複数の画像をダウンロードできたものから表示
AsyncImage, placeholder, List, LazyVGrid, NavigationView, NavigationLink

ContentView.swift

struct Photo: Identifiable {
    var id: URL{url}
    var url: URL
}

class PhotoSource {
    var photos: [Photo] = []
    init() {
        photos = makePhotos()
    }
}

extension PhotoSource {
    func makePhotos() -> [Photo] {
        let path = "https://oshige.xsrv.jp/samples/photos/"
        let photoNames: [String] = [
            "IMG_1159.jpg", "IMG_1326.jpg", "IMG_1384.jpg", "IMG_1475.jpg",
            "IMG_1476.jpg", "IMG_1478.jpg", "IMG_1635.jpg", "IMG_1643.jpg",
            "IMG_1739.jpg", "IMG_1840.jpg", "IMG_1889.jpg", "IMG_2233.jpg",
            "IMG_2325.jpg", "IMG_2406.jpg", "IMG_2408.jpg", "IMG_4008.jpg"
        ]
        
        var photos: [Photo] = []
        for name in photoNames {
            photos.append(Photo(url: URL(string: path + name)!))
        }
        return photos
    }
}

struct ContentView: View {
    
    private var myPhotoSource = PhotoSource()
    
    var body: some View {
        NavigationView {
            List(myPhotoSource.photos) {
                photo in
                
                AsyncImage(url: photo.url) {
                    image in
                    
                    image.resizable()
                        .aspectRatio(contentMode: .fit)
                } placeholder: {
                    Color.orange
                        .overlay(Image(systemName: "photo").scaleEffect(2.0))
                }
                .mask(RoundedRectangle(cornerRadius: 16))
                .frame(height: 160)
            }
            .navigationTitle("お気に入り")
        }
    }
}

なるほど、非同期というとJSって感じだが、Swiftでもあるのね。

[Swift] 現在地を表示して移動をフォロー

LocationManager.swift

class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
    
    let manager = CLLocationManager()
    @Published var region = MKCoordinateRegion()
    
    override init() {
        super.init()
        manager.delegate = self
        manager.requestWhenInUseAuthorization()
        manager.desiredAccuracy = kCLLocationAccuracyBest
        manager.distanceFilter = 2
        manager.startUpdatingLocation()
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]){
        locations.last.map {
            let center = CLLocationCoordinate2D(
                latitude: $0.coordinate.latitude,
                longitude: $0.coordinate.longitude
            )
            
            region = MKCoordinateRegion(
                center: center,
                latitudinalMeters: 1000.0,
                longitudinalMeters: 1000.0
            )
        }
    }
}

ContentView.swfit

import SwiftUI
import MapKit

struct ContentView: View {
    
    @ObservedObject var manager = LocationManager()
    @State var trackingMode = MapUserTrackingMode.follow

    var body: some View {
        Map(coordinateRegion: $manager.region,
            showsUserLocation: true,
            userTrackingMode: $trackingMode)
            .edgesIgnoringSafeArea(.bottom)
    }
}

まずまず

[Swift] 地図にアノテーションを表示

ContentView.swift

import SwiftUI
import MapKit

struct Spot: Identifiable {
    let id = UUID()
    let latitude: Double
    let longitude: Double
    var coordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    }
}

struct ContentView: View {
    
    let spotlist = [
        Spot(latitude: 35.6834843, longitude: 139.7644207),
        Spot(latitude: 35.6790079, longitude: 139.7675881),
        Spot(latitude: 35.6780057, longitude: 139.7631035)
    ]
    
    @State var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(
            latitude: 35.6805702,
            longitude: 139.7675359
        ),
        latitudinalMeters: 1000.0,
        longitudinalMeters: 1000.0
    )

    var body: some View {
        Map(coordinateRegion: $region,
            annotationItems: spotlist,
            annotationContent: {spot in MapMarker(coordinate: spot.coordinate, tint: .orange)})
            .edgesIgnoringSafeArea(.bottom)
    }
}

この辺はGoogleMapAPIと似てるなー

[Swift] Map()で地図を作る

Map()で地図を表示する

import SwiftUI
import MapKit

struct ContentView: View {
    
    @State var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(
            latitude: 35.6805702,
            longitude: 139.7675359
        ),
        latitudinalMeters: 1000.0,
        longitudinalMeters: 1000.0
    )

    var body: some View {
        Map(coordinateRegion: $region)
            .edgesIgnoringSafeArea(.bottom)
    }
}

地図機能はいつ見ても興奮します。

[Swift] Color拡張

ContentView.swift

extension Color {
    static var hagiiro: Color {
        return Color(red: 223/255, green: 87/255, blue: 143/255, opacity: 1.0)
    }
}

struct ContentView: View {

    var body: some View {
        ZStack{
            Circle()
                .foregroundColor(.hagiiro)
                .frame(width: 200, height:200)
            Text("萩色")
                .foregroundColor(.white)
                .font(.title)
        }
    }
}

そりゃできるよね。

[Swift] クラス定義

クラスはオブジェクトの仕様を定義したもの
L どんな属性と機能を持ち合わせたオブジェクトを作るのかを定義した設計書
L クラス定義に基づいて作ったオブジェクトをインスタンスと言う

classの書き方

class className {
	var variable:type = initial

	init(引数:type) {
		 self.variable = 引数
	}

	function fun(引数:type) -> void {
	 	return x
	}
}

playground

import UIKit

var greeting = "Hello, playground"

class MyFriend {
    var name:String
    var age:Int
    
    init(name:String, age:Int){
        self.name = name
        self.age = age
    }
    
    func hello() -> String {
        let message = "Hello! \(name)です。\(age)歳です。"
        return message
    }
}

let friend1 = MyFriend(name: "植木", age: 31)
let friend2 = MyFriend(name: "桜", age: 26)

let str1 = friend1.name + "と" + friend2.name + "は友達です。"
let str2 = friend1.name + "は" + String(friend1.age) + "歳です。"
friend2.age += 1
let str3 = friend2.name + "は誕生日で" + String(friend2.age) + "歳になりました。"

print(str1)
print(str2)
print(str3)

### 継承

class GoodFriend: MyFriend {
    let fortune = ["大吉", "吉", "小吉", "凶"]
    
    func uranai() -> String {
        let index = Int.random(in: 0 ..<fortune.count)
        let result = "今日の運勢は" + fortune[index] + "です!"
        return result
    }
    
    func who() -> String {
        return name + "です。よろしく!"
    }
}

### スーパークラス初期化

    var nickname:String
    init(name: String, age: Int, nickname: String){
        self.nickname = nickname
        super.init(name: name, age:age)
    }

### extensionを使ってクラス拡張

class Player {
    var name: String = ""
    func hello() {
        print("やあ!" + name)
    }
}

extension Player {
    var who: String {
        get {
            return name
        }
        set(value){
            name = value
        }
    }
    func bye(){
        print("またね!" + name)
    }
}

let user = Player()
user.who = "賢治"
user.hello()
user.bye()

これは数重ねるだけだなー