| ページ一覧 | ブログ | twitter |  書式 | 書式(表) |

MyMemoWiki

SwiftUI

提供: MyMemoWiki
2021年5月12日 (水) 11:33時点におけるPiroto (トーク | 投稿記録)による版 (→‎Tab切り替えイベント)
ナビゲーションに移動 検索に移動

| Swift | Mac | Xcode | Swift Sample |

目次

SwiftUI

  • SwiftUI
  • SwiftUI Documents
  • macos tutorials
  • 1セットのツールとAPIを使用するだけで、あらゆるAppleデバイス向けのユーザーインターフェイスを構築
  • 宣言型シンタックスを使
  • 宣言型のスタイルは、アニメーションなどの複雑な概念にも適用

デザインツール

  • Xcodeには、SwiftUIでのインターフェイス構築をドラッグ&ドロップのように簡単に行える直感的な新しいデザインツールが含まれています
  • デザインキャンバスでの編集内容と、隣接するエディタ内のコードはすべて完全に同期されます

ドラッグ&ドロップ

  • ユーザーインターフェイス内のコンポーネントの位置は、キャンバス上でコントロールをドラッグするだけで調整できます

ダイナミックリプレースメント

  • wiftのコンパイラとランタイムはXcode全体に完全に埋め込まれているため、Appは常にビルドされ実行されます
  • 表示されるデザインキャンバスは、単にユーザーインターフェイスに似せたものではなく、実際のAppそのもの
  • Xcodeは編集したコードを実際のAppに直接組み入れることができます

プレビュー

  • プレビューを1つまたは複数作成して、サンプルデータを取得できる

Swift UI チュートリアルをやってみる

プロジェクト作成〜TextViewのカスタマイズ

Custom Image Viewの作成

Xcodeを使ってmacOS プログラミングとplaygroundの作成

Listとナビゲーションとプレビュー

レイアウト

余白の取り方


  • backgroundにpaddingを指定する(cssのmargin的な効果)

Swiftui padding2.png

  Text(host.host)
       .background(Color.green)
       .padding()
  • Textにpaddingを指定することになる(cssのpadding的効果)

Swiftui padding1.png

  Text(host.host)
       .padding()
       .background(Color.green)

ボタンサイズ


  • ボタンのサイズを内容にフィットさせたい

Swiftui button size1.png

  Button(action: {}) {
     VStack{
         :
     }
    .padding()
    .border(Color.blue, width: 3)
  }
  • .buttonStyle(PlainButtonStyle()) を指定

Swiftui button size2.png

Button(action: {}) {
   VStack{
         :
     }
    .padding()
    .border(Color.blue, width: 3)
}.buttonStyle(PlainButtonStyle())

親Viewのサイズ情報を取得する


Swiftui grid layout1.png Swiftui grid layout2.png

struct HostView : View {
    var host: WoL.Host
    var body: some View {
        GeometryReader { geo in
            HStack() {
                Text(host.host)
                    .padding()
                    .frame(width:  geo.size.width * 0.33 , alignment: .leading)
                    .frame(maxHeight: .infinity)
                    .background(Color.red)
                Text(host.ip)
                    .padding()
                    .frame(width: geo.size.width * 0.33 , alignment: .leading)
                    .frame(maxHeight: .infinity)
                    .background(Color.green)
                Text(host.macaddr)
                    .padding()
                    .frame(width: geo.size.width * 0.33 , alignment: .leading)
                    .frame(maxHeight: .infinity)
                    .background(Color.yellow)
            }.fixedSize(horizontal: false, vertical: true)
            
        }.frame(height: 60)
    }
}

影をつける

---

  • shadowを設定すると、内部のコンポーネント全てに影がつく、compositingGroup を指定することで、背景だけに影をつけることができる
   
             :
        .background()
        .compositingGroup()
        .shadow(radius: 10)

Card View


Swiftui card layout1.png Swiftui card layout2.png

            let columns =
                [GridItem(.adaptive(minimum: 250, maximum: 800))]
            ScrollView {
                LazyVGrid(columns: columns, spacing:10) {
                    ForEach(hosts.hosts, id: \.macaddr) { host in
                        HostView(host:host)
                    }
                } .padding(20)

Toolbar


300lx

ScrollView {
	LazyVGrid(columns: columns) {
		ForEach(hosts.hosts, id: \.ip) { host in
			HostView(host:host)
		}
	} .padding(20)
}.toolbar {
	ToolbarItem(placement: .automatic) {
		Button("arp -a") {
			WoLService().arp(hosts:self.hosts)
		}
	}
	ToolbarItem(placement: .automatic) {
		Button("load") {
			WoLService().load(hosts:self.hosts)
		}
	}
}

データ

UserDefaults


ドキュメントパス


let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
print("Document path: \(paths)")
  • 出力
Document path: [file:///Users/hirotoyagi/Library/Containers/info.typea.WoL/Data/Documents/]

ファイル格納先


View間データ受け渡し


ObservableObject を経由して親子ViewでAlertの表示フラグを共有

Swiftui share object.png

  • ObservableObjectを共有して、変更をSubscribeする
struct HostListView: View {
    @ObservedObject var hosts = HostList()
    @ObservedObject var param = Param()
    var body: some View {
        VStack {
            let columns =
                [GridItem(.adaptive(minimum: 250, maximum: 800))]
            ScrollView {
                LazyVGrid(columns: columns, spacing:10) {
                    ForEach(hosts.hosts, id: \.id) { host in
                        HostView(host:host).environmentObject(param)
                    }
                } .padding(20)
            }.alert(isPresented: $param.isSaveAlert, content: {
                Alert(title: Text("Title"),message: Text("Messge"),
                    primaryButton: .default(Text("OK"), action: {}),
                    secondaryButton: .cancel(Text("Cancel"),action: {}))
            })
        }
    }
}
struct HostView : View {
    @ObservedObject var host: WoL.Host
    @EnvironmentObject var param: Param
    
    var body: some View {
        VStack{
            HStack {
                Button(action: {
                    self.param.isSaveAlert = true
                }) {
                    Image(systemName: "square.and.arrow.down")
                }.help(Text("save"))
            }
               :            
        }
    }
}

class Param : ObservableObject {
    @Published var isSaveAlert: Bool = false
}

@Binding を経由して親子ViewでAlertの表示フラグを共有

Swiftui share object.png

  • @Binding でView間で変数を共有
  • 呼び出し側は@State
struct HostListView: View {
    @ObservedObject var hosts = HostList()
    @State var isSaveAlert = false

    var body: some View {
        VStack {
            let columns =
                [GridItem(.adaptive(minimum: 250, maximum: 800))]
            ScrollView {
                LazyVGrid(columns: columns, spacing:10) {
                    ForEach(hosts.hosts, id: \.id) { host in
                        HostView(host:host, isSaveAlert: $isSaveAlert, alertMessage: $alertMessage)
                    }
                } .padding(20)
            }.alert(isPresented: $isSaveAlert, content: {
                Alert(title: Text("Title"),message: Text(alertMessage),
                      primaryButton: .default(Text("OK"), action: {}),
                      secondaryButton: .cancel(Text("Cancel"),action: {}))
            })
        }
    }
}
struct HostView : View {
    @ObservedObject var host: WoL.Host
    @Binding var isSaveAlert: Bool
    
    var body: some View {
        VStack{
            HStack {
                Button(action: {
                    self.isSaveAlert = true
                }) {
                    Image(systemName: "square.and.arrow.down")
                }.help(Text("save"))
                 :
			}
        }
    }
}
@Binding使用時のPreview
  • @Stateかつ、staticで宣言
  • $で変数を渡す
struct HostView_Previews: PreviewProvider {
    @State static var isAlert = true

    static var previews: some View {
        let host = WoL.Host(host: "test.local", ip: "192.168.0.1", macaddr: "aa:bb:cc:dd:ee:ff", comment: "")
        HostView(host: host, isAlert: $isAlert)
    }
}

メニュー

メインメニューに別のViewを開くメニューを追加


Swift ui main menu.png

  • .commandsを記述
@main
struct WoLApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }.commands {
            CommandGroup(after: CommandGroupPlacement.appInfo) {
                Divider()
                NavigationLink(destination: PreferenceView()) {
                    Text("preferences")
                }
            }
        }
    }
}

図形

Capsule


Swiftui capsule.png

VStack{
   :
}
.padding()
.background(
	Capsule(style: .continuous)
		.foregroundColor(Color.white)
)
.shadow(radius:10 )

RoundedRectangle


Swiftui roundedRectangle.png

VStack{
  :
}
.padding()
.background(
	RoundedRectangle(cornerRadius: 20)
		.foregroundColor(Color.white)
)
.shadow(radius:10 )

画像

SF Symbol アイコン


コードサンプル(コンポーネント)

Button


Swift sample button.png

import Foundation
import SwiftUI

struct ButtonView: View {

    @State var cnt:Int = 0;
    var body: some View {
        VStack {
            Button(action: {
                self.cnt += 1;
                print("print \(self.cnt)")
            })
            {
                Text("Button+1 (\(self.cnt))")
            }
            .padding(.horizontal, 25.0)
            .font(.largeTitle)
            .foregroundColor(Color.white)
            .background(Color.green)
            .cornerRadius(15, antialiased: true)
            Divider()
            Button("Button+2 (\(self.cnt))") {
                self.cnt += 2;
            }
            .font(.largeTitle)
            .foregroundColor(.white)
            .background(
                Capsule()
                    .foregroundColor(Color.blue)
                    .frame(width: 200, height: 60, alignment: .center)
            )
        }
    }
}

Toggle(@State)


Swift sample toggle.png

import SwiftUI

struct ToggleView: View {
    @State var isOn = true
    var body: some View {
        VStack {
            Toggle(isOn: $isOn) {
                Text("On/Off")
                    .font(.title)
            }
            .fixedSize()
            .padding()
            
            /* https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/
             */
            Button(action: {
                withAnimation {
                    self.isOn.toggle()
                }
            }) {
                Image(systemName: self.isOn ? "applewatch" : "applewatch.slash")
                    .font(.system(size: 60))
                    .frame(width: 100, height: 100)
                    .imageScale(.large)
                    .rotationEffect(.degrees(isOn ? 0 : 360))
                    
            }
        }
    }
}

Stepper


Swift sample stepper.png

import SwiftUI

struct StepperView: View {
    @State var cnt = 0;
    var body: some View {
        VStack {
            Stepper(value: $cnt, in: 0 ... 5) {
                Text("Stepper-\(self.cnt)")
            }.frame(width: 200)
            Stepper(
                onIncrement: {
                    self.cnt += 5;
                },
                onDecrement: {
                    self.cnt -= 3;
                },
                label: {
                    Text("Stepper-\(self.cnt)")
                }
            ).frame(width: 200)
        }
    }
}

Alert


Swift sample alert.png

import SwiftUI

struct AlertView: View {
    @State var isAlert = false;
    var body: some View {
        Button(action: {
            self.isAlert = true
        }) {
            Text("Alert")
                .foregroundColor(.white)
        }.background(
            Capsule()
                .foregroundColor(.blue)
                .frame(width: 100, height: 40)
        ).alert(isPresented: $isAlert, content: {
            Alert(title: Text("Title"),message: Text("Messge"),
                primaryButton: .default(Text("OK"), action: {}),
                secondaryButton: .cancel(Text("Cancel"),action: {}))
        })
    }
}

Tab


Swift sample tab.png


struct TabbedView: View {
    @State var selection = 0
    var body: some View {
        TabView(selection: $selection) {
            ButtonView().tabItem {
                Text("Item1")
            }.tag(1)
            ToggleView().tabItem {
                Text("Item2")
            }.tag(2)
        }
    }
}

Tab切り替えイベント


    @State var selectedTabIndex = 1;
    var body: some View {
        TabView(selection: $selectedTabIndex) {
            arpListView.tabItem {
                Text("Tab Label 1")
            }.tag(1)
            hostListView.tabItem {
                Text("Tab Label 2")
            }.tag(2)
        }
        .onChange(of: selectedTabIndex) { value in
            print("TAB INDEX \(value)")
        }
    }

Binding


Swift sample binding.png

import SwiftUI

struct BindingView: View {
    @State var isChecked1: Bool = false
    @State var isChecked2: Bool = false
    var body: some View {
        VStack {
            CheckImageButton(isChecked: $isChecked1)
            CheckImageButton(isChecked: $isChecked2)
        }
    }
}

struct CheckImageButton: View {
    @Binding var isChecked: Bool
    var body: some View {
        Button(action: {
            self.isChecked.toggle()
        }) {
            Image(systemName: isChecked ?
                "person.crop.circle.badge.checkmark":
                "person.crop.circle")
                .foregroundColor(
                    isChecked ? .blue : .gray)
        }
        .imageScale(.large)
        .frame(width: 40)
    
    }
}

画像


Swiftui image sample.png

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image("add_component_swiftui").resizable()
                .aspectRatio(contentMode:.fill)
                .frame(width: 400, height: 400)
                .scaleEffect(1.2)
                .offset(x: -60, y: 0)
                .clipped()
                .overlay(
                    Text("SwiftUI Sample")
                        .font(.title)
                        .foregroundColor(.red)
                )
                .clipShape(Circle()/)
                .shadow(radius: 10)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

図形


Swiftui sample figure.png

import SwiftUI

struct Figure: View {
    var body: some View {
          VStack {
              Circle()
                  .foregroundColor(.blue)
                  .frame(width: 100.0, height:100.0)
              Ellipse()
                  .foregroundColor(.green)
                  .frame(width: 200, height: 100.0)
              Rectangle()
                  .foregroundColor(.orange)
                  .frame(width: 100.0, height: 100.0)
                  .rotationEffect(.degrees(45))
            }
    }
}

struct Figure_Previews: PreviewProvider {
    static var previews: some View {
        Figure()
    }
}

List


Swiftui list sample.png

import SwiftUI

struct ListView: View {
    var body: some View {
        NavigationView {
            List {
                Text("List Item")
                Text("List Item")
                HStack {
                    Image("add_component_swiftui")
                        .frame(width: 100, height: 100)
                        .scaleEffect(0.2)
                        .aspectRatio(contentMode:.fit)
                        .clipped()
                        .clipShape(/*@START_MENU_TOKEN@*/Circle()/*@END_MENU_TOKEN@*/)
                       
                }
                Text("List Item")
                Text("List Item")
            }.navigationBarTitle("List Title")
        }
    }
}

struct ListView_Previews: PreviewProvider {
    static var previews: some View {
        ListView()
    }
}

List(繰り返し、Section)


Swiftui list section.png

import SwiftUI

let items = ["item1","item2","item3","item4","item5"];
struct EmbededList: View {
    var body: some View {
        VStack {
            List(0..<items.count) { idx in
                Text(items[idx])
            }
            .frame(height: 300.0)
            List {
                Section(header: Text("Section1")) {
                    ForEach(0 ..< items.count) { idx in
                        Text(items[idx])
                    }
                }
                Section(header: Text("Section2")) {
                    ForEach(0 ..< items.count) { idx in
                        Text(items[idx])
                    }
                }
            }
        }
    }
}

struct EmbededList_Previews: PreviewProvider {
    static var previews: some View {
        EmbededList()
    }
}

Listにオブジェクトを表示


Swift ui object load to list.png

import SwiftUI

struct ContentView: View {
    @ObservedObject var hosts = HostList()
    var body: some View {
        VStack {
            HStack {
                Button(action: {
                    WoLService().arp(hosts:self.hosts)
                }) {
                    Text("arp -a")
                }.padding()
            }
            Divider()
            List (hosts.hosts, id: \.ip){ host in
                Text(host.host)
                Text(host.ip)
                Text(host.macaddr)
            }
        }
    }
}
import Foundation

class HostList : ObservableObject {
    @Published var hosts: [Host] = []
}

class Host {
    var host: String = ""
    var ip: String = ""
    var macaddr: String = ""
}

コードサンプル(ロジック)

Observable(@ObservedObject,@Published,@State)


  1. データクラスはObservableObjectプロトコル準拠とする。
  2. 監視対象とするプロパティに@Published属性を付加する。
  3. データクラスのインスタンスは@ObservedObject属性を付加してViewの中で宣言する

Swift sample observable.png

  • Publish
import Foundation

class PublishObject: ObservableObject {
    @Published var counter: Int = 0
    
    var timer = Timer()
    
    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.counter += 1
        }
    }
    
    func stop() {
        timer.invalidate()
    }
    
    func reset() {
        timer.invalidate()
        counter = 0
    }
}
  • Subscribe
import SwiftUI

struct SubscriberView: View {
    @ObservedObject var publisher = PublishObject()
    
    let currentTimer = Timer.TimerPublisher(interval: 1.0, runLoop: .main, mode: .default).autoconnect()
    @State var now = Date()
    
    var body: some View {
        VStack {
            Text("\(self.now.description)")
            HStack {
                Button(action: {
                    self.publisher.start()
                }){
                    Image(systemName: "play")
                }.padding()
                Button(action: {
                    self.publisher.stop()
                }){
                    Image(systemName: "pause")
                }.padding()
                Button(action: {
                    self.publisher.reset()
                }){
                    Image(systemName: "backward.end")
                }.padding()
            }
            .frame(width:200)
        
            Text("\(self.publisher.counter)")
            
        }.font(.largeTitle)
        .onReceive(currentTimer) { date in
            self.now = date
        }
    }
}

動的に検索

Swiftui dynamic search.png

struct HostListView: View {
    @ObservedObject var hosts = HostList()
    @ObservedObject var param = HostViewParameter()
    // 検索キーワード
    @State var searchKeyword = ""
    
    var body: some View {
        VStack {
            let columns =
                [GridItem(.adaptive(minimum: 250, maximum: 800))]
            ScrollView {
                LazyVGrid(columns: columns, spacing:10) {
                    // Arrayにfilterを適用
                    ForEach(hosts.hosts.filter({ (host) -> Bool in
                        if searchKeyword != "" {
                            return host.host.contains(searchKeyword) ||
                                host.ip.contains(searchKeyword) ||
                                host.comment.contains(searchKeyword)
                        }
                        return true
                    }), id: \.id) { host in
                        HostView(host:host).environmentObject(param)
                    }
                } .padding(20)
            }.toolbar {
                        :
               ToolbarItem(placement: .automatic) {
                    TextField("search...", text: $searchKeyword)
                }
            }
        }
    }
}

バックグラウンドからUIを操作する


  • observableobj が、ObservableObject の派生クラス
  • contentフィールドに、@Published アノテーション
  • Viewで、@ObservedObjectを付与しインスタンスを生成
  • 上記で、バックグラウンドから、observableobj.contentを操作すると、UIはメインスレッドから触るように怒られる。

Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.

  • DispatchQueue.main.syncで囲む
DispatchQueue.main.sync {
    observableobj.content = text
}

Tips


画面部品の追加方法


SwiftUIライブラリ


SwiftUIX

SwiftUIアプリケーション開発の不足を補うSwiftUIX