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

MyMemoWiki

SwiftUI

提供: MyMemoWiki
ナビゲーションに移動 検索に移動

| Swift | Mac | Xcode | Swift Sample | Cocoa | Xamarin.Mac |

目次

SwiftUI

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

デザインツール

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

ドラッグ&ドロップ

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

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

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

プレビュー

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

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


Xcode ナビゲーター


  左から

  1. プロジェクトナビゲーター
  2. ソースコントロールナビゲーター
  3. シンボルナビゲーター
  4. 検索ナビゲーター
  5. イシューナビゲーター
  6. テストナビゲーター
  7. デバッグナビゲーター
  8. ブレークポイントナビゲーター
  9. レポートナビゲーター

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

Custom Image Viewの作成

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

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

レイアウト

Alignment


余白の取り方


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

 

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

 

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

ボタンサイズ


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

 

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

 

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

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


   

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


   

            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


 

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

データ

データ変更に応じて画面に反映させる

  • 画面の構造体の中でデータを保持しているクラスのインスタンスを格納するプロパティに、@ObservedObjectプロパティラッパーを付与する
  •  データを保持しているクラスを、@ObservableObjectプロトコルに準拠させる
  • クラスの中で変更を反映させる値を保持しているプロパティに、@Publishedプロパティラッパーを付与する

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間データ受け渡し


SwiftUIでは親ビューと子ビュー間で値を渡す方法は、下記の3つが挙げられます。

  1. Environment
    1. Viewが持つ環境変数。独自の環境変数を定義することができ、それを利用して親ビューから任意の値を渡すことが可能
  2. EnvironmentObjects
    1. 他の2つに比べて一般的な方法。利用するためには独自のクラスを定義
  3. Preferences
    1. Preferenceは子から親へ伝達させる方法



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

 

  • 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の表示フラグを共有

 

  • @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を開くメニューを追加


 

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

図形

Capsule


 

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

RoundedRectangle


 

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

画像

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

Button


 

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)


 

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


 

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


 

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


 


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


 

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

リストのBinding


 

  • Model
import Foundation
import AppKit
import SwiftUI

class File : Identifiable, ObservableObject {
    let id = UUID()
    let path: String
    let name: String
    let isDirectory: Bool
    var isOn: Bool = false
    
    init(path: String, name: String, isDirectory: Bool, isOn: Bool) {
        self.path = path
        self.name = name
        self.isDirectory = isDirectory
        self.isOn = isOn
    }
}

class FileList : ObservableObject {
    @Published var files: [File] = []
}
  • UI
    • 各所(①②③)に$をつける
import SwiftUI

struct ContentView: View {
    @ObservedObject var filePaths = FileList()
    @State private var selectedPaths = Set<File.ID>()
    
    var body: some View {
        Button(action: {
            let rootDir = EncConverterService.chooseDir()
            let queue = DispatchQueue.global(qos: .userInitiated)
            queue.async {
                EncConverterService.loadFile(directoryPath: rootDir, filepaths: self.filePaths)
            }
        }) {
            Text("Choose dir")  
        }
        
        Table($filePaths.files, selection: $selectedPaths) {   // ①
            TableColumn("path") { $file in  // ②
                let isDir = file.isDirectory
                HStack {
                    if !isDir {
                        Toggle("",isOn: $file.isOn) // ③
                            .padding(.leading, (isDir) ? 0 : 20)
                    }
                    Text((isDir) ? file.path : file.name)
                        .font((isDir) ? .headline : .none)
                        
                }
            }
        }
        Text("\(selectedPaths.count) path selected")
    }
}

画像


 

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

図形


 

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


 

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)


 

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にオブジェクトを表示


 

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 = ""
}

Table


 

  • Service
import Foundation
import AppKit
import SwiftUI

class File : Identifiable {
    let id = UUID()
    let path: String
    
    init(path: String) {
        self.path = path
    }
}

class FileList : ObservableObject {
    @Published var files: [File] = []
}

public struct EncConverterService {
    static func chooseDir() -> String {
        
        let dialog = NSOpenPanel();

        dialog.title                   = "Choose a file| Our Code World"
        dialog.showsResizeIndicator    = true
        dialog.showsHiddenFiles        = false
        dialog.allowsMultipleSelection = false
        dialog.canChooseDirectories    = true
        dialog.canChooseFiles          = false

        if (dialog.runModal() ==  NSApplication.ModalResponse.OK) {
            let result = dialog.url

            if (result != nil) {
                let path: String = result!.path
                return path
            }
            
        }
        return "";
    }
    
    static func loadFile(directoryPath: String, filepaths: FileList) {
        print(directoryPath)
        filepaths.files.append(File(path: directoryPath))
        do {
            let fm = FileManager.default
            let fileNames = try fm.contentsOfDirectory(atPath: directoryPath)
            
            for fileName in fileNames {
                let childPath = directoryPath + "/" + fileName
                
                var isDir = ObjCBool(false)
                fm.fileExists(atPath: childPath, isDirectory: &isDir)
                if isDir.boolValue {
                    loadFile(directoryPath: childPath, filepaths: filepaths)
                }
                print("\t" + childPath)
                filepaths.files.append(File(path: childPath))
            }
        } catch {
            print(error)
        }
    }
}
  • View
import SwiftUI

struct ContentView: View {
    @ObservedObject var filePaths = FileList()
    @State private var selectedPaths = Set<File.ID>()
    
    var body: some View {
        Button(action: {
            let rootDir = EncConverterService.chooseDir()
            let queue = DispatchQueue.global(qos: .userInitiated)
            queue.async {
                EncConverterService.loadFile(directoryPath: rootDir, filepaths: self.filePaths)
            }
        }) {
            Text("Choose dir")
        }
        
        Table(filePaths.files, selection: $selectedPaths) {
            TableColumn("Path", value: \.path)
        }
        Text("\(selectedPaths.count) path selected")

    }
}

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

Observable(@ObservedObject,@Published,@State)


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

 

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

動的に検索

 

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


ファイルパーミッションエラー(App Sandbox)


画面部品の追加方法


SwiftUIライブラリ


SwiftUIX

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

ファイル選択


 let dialog = NSOpenPanel();

dialog.title                   = "Choose a file"
dialog.showsResizeIndicator    = true
dialog.showsHiddenFiles        = false
dialog.allowsMultipleSelection = false
dialog.canChooseDirectories = false

if (dialog.runModal() ==  NSApplication.ModalResponse.OK) {
	let result = dialog.url 
	if (result != nil) {
		let path: String = result!.path
		print(path)
	}
	
} else {
	return
}