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

MyMemoWiki

「SwiftUI」の版間の差分

提供: MyMemoWiki
ナビゲーションに移動 検索に移動
 
(同じ利用者による、間の85版が非表示)
1行目: 1行目:
| [[Swift]] | [[Mac]] | [[Xcode]] | [[Swift Sample]] |
+
| [[Swift]] | [[Mac]] | [[Xcode]] | [[Swift Sample]] | [[Cocoa]] | [[Xamarin.Mac]] |
  
 
{{amazon|B082SMJC7V}}
 
{{amazon|B082SMJC7V}}
23行目: 23行目:
  
 
===Swift UI チュートリアルをやってみる===
 
===Swift UI チュートリアルをやってみる===
 +
----
 +
*[https://www.typea.info/blog/index.php/2020/12/09/swiftui_tutorial_youtube_memo/  プロジェクト作成〜TextViewのカスタマイズ]
 +
*[[Xcode]]
 +
*resume -> プレビュー
 +
*command + click -> Action List
 +
*右上の + ボタンでコントロール追加
 +
 +
====[[Xcode]] ナビゲーター====
 +
----
 +
[[File:xcode_navgator_icons.png|400px]]
 +
左から
 +
#プロジェクトナビゲーター
 +
#ソースコントロールナビゲーター
 +
#シンボルナビゲーター
 +
#検索ナビゲーター
 +
#イシューナビゲーター
 +
#テストナビゲーター
 +
#デバッグナビゲーター
 +
#ブレークポイントナビゲーター
 +
#レポートナビゲーター
 +
 
====[https://www.typea.info/blog/index.php/2020/12/09/swiftui_tutorial_youtube_memo/  プロジェクト作成〜TextViewのカスタマイズ]====
 
====[https://www.typea.info/blog/index.php/2020/12/09/swiftui_tutorial_youtube_memo/  プロジェクト作成〜TextViewのカスタマイズ]====
 
====[https://www.typea.info/blog/index.php/2020/12/19/swiftui_tutorial_custom_image_view/  Custom Image Viewの作成]====
 
====[https://www.typea.info/blog/index.php/2020/12/19/swiftui_tutorial_custom_image_view/  Custom Image Viewの作成]====
 
====[https://www.typea.info/blog/index.php/2020/12/06/xcode_macos_proguramming/  Xcodeを使ってmacOS プログラミングとplaygroundの作成]====
 
====[https://www.typea.info/blog/index.php/2020/12/06/xcode_macos_proguramming/  Xcodeを使ってmacOS プログラミングとplaygroundの作成]====
 
====[https://www.typea.info/blog/index.php/2021/01/23/swiftui_tutorial_list_navigation/  Listとナビゲーションとプレビュー]====
 
====[https://www.typea.info/blog/index.php/2021/01/23/swiftui_tutorial_list_navigation/  Listとナビゲーションとプレビュー]====
 +
 +
==レイアウト==
 +
===[https://d1v1b.com/swiftui/alignment Alignment]===
 +
----
 +
*[https://d1v1b.com/swiftui/alignment Alignment]
 +
 +
===[https://t32k.me/mol/log/margin-padding-swiftui/ 余白の取り方]===
 +
----
 +
*[https://t32k.me/mol/log/margin-padding-swiftui/ 余白の取り方]
 +
*記述箇所によって、表示が変わる
 +
 +
*backgroundにpaddingを指定する(cssのmargin的な効果)
 +
[[File:swiftui_padding2.png|300px]]
 +
<pre>
 +
  Text(host.host)
 +
      .background(Color.green)
 +
      .padding()
 +
</pre>
 +
*Textにpaddingを指定することになる(cssのpadding的効果)
 +
[[File:swiftui_padding1.png|300px]]
 +
<pre>
 +
  Text(host.host)
 +
      .padding()
 +
      .background(Color.green)
 +
</pre>
 +
 +
===ボタンサイズ===
 +
----
 +
*ボタンのサイズを内容にフィットさせたい
 +
[[File:swiftui_button_size1.png|300px]]
 +
<pre>
 +
  Button(action: {}) {
 +
    VStack{
 +
        :
 +
    }
 +
    .padding()
 +
    .border(Color.blue, width: 3)
 +
  }
 +
</pre>
 +
*.buttonStyle(PlainButtonStyle()) を指定
 +
[[File:swiftui_button_size2.png|300px]]
 +
<pre>
 +
Button(action: {}) {
 +
  VStack{
 +
        :
 +
    }
 +
    .padding()
 +
    .border(Color.blue, width: 3)
 +
}.buttonStyle(PlainButtonStyle())
 +
</pre>
 +
===親Viewのサイズ情報を取得する===
 +
----
 +
*https://qiita.com/masa7351/items/0567969f93cc88d714ac
 +
*https://www.hackingwithswift.com/quick-start/swiftui/how-to-make-two-views-the-same-width-or-height
 +
[[File:swiftui_grid_layout1.png|200px]]
 +
[[File:swiftui_grid_layout2.png|400px]]
 +
<pre>
 +
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)
 +
    }
 +
}
 +
</pre>
 +
 +
===影をつける===
 +
---
 +
*shadowを設定すると、内部のコンポーネント全てに影がつく、compositingGroup を指定することで、背景だけに影をつけることができる
 +
<pre> 
 +
            :
 +
        .background()
 +
        .compositingGroup()
 +
        .shadow(radius: 10)
 +
</pre>
 +
===Card View===
 +
----
 +
*[https://www.hackingwithswift.com/books/ios-swiftui/designing-a-single-card-view Card View]
 +
*LazyGrid
 +
[[File:swiftui_card_layout1.png|400px]]
 +
[[File:swiftui_card_layout2.png|200px]]
 +
<pre>
 +
            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)
 +
</pre>
 +
 +
===Toolbar===
 +
----
 +
[[File:swiftui_toolbar.png|300lx]]
 +
<pre>
 +
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)
 +
}
 +
}
 +
}
 +
</pre>
 +
 +
==データ==
 +
=== データ変更に応じて画面に反映させる ===
 +
* 画面の構造体の中でデータを保持しているクラスのインスタンスを格納するプロパティに、@ObservedObjectプロパティラッパーを付与する
 +
* データを保持しているクラスを、@ObservableObjectプロトコルに準拠させる
 +
* クラスの中で変更を反映させる値を保持しているプロパティに、@Publishedプロパティラッパーを付与する
 +
 +
=== UserDefaults===
 +
----
 +
 +
===ドキュメントパス===
 +
----
 +
<pre>
 +
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
 +
print("Document path: \(paths)")
 +
</pre>
 +
*出力
 +
<pre>
 +
Document path: [file:///Users/hirotoyagi/Library/Containers/info.typea.WoL/Data/Documents/]
 +
</pre>
 +
====ファイル格納先====
 +
----
 +
*[https://qiita.com/am10/items/3b2eb3d9f6c6955455b6 ファイル格納先]
 +
===View間データ受け渡し===
 +
----
 +
SwiftUIでは親ビューと子ビュー間で値を渡す方法は、下記の3つが挙げられます。
 +
 +
#Environment
 +
##Viewが持つ環境変数。独自の環境変数を定義することができ、それを利用して親ビューから任意の値を渡すことが可能
 +
#EnvironmentObjects
 +
##他の2つに比べて一般的な方法。利用するためには独自のクラスを定義
 +
#Preferences
 +
##Preferenceは子から親へ伝達させる方法
 +
 +
 +
 +
 +
*https://qiita.com/noby111/items/26405bd89075c841029a
 +
====ObservableObject を経由して親子ViewでAlertの表示フラグを共有====
 +
[[File:swiftui_share_object.png|300px]]
 +
*ObservableObjectを共有して、変更をSubscribeする
 +
<pre>
 +
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
 +
}
 +
</pre>
 +
 +
====@Binding を経由して親子ViewでAlertの表示フラグを共有====
 +
[[File:swiftui_share_object.png|300px]]
 +
*@Binding でView間で変数を共有
 +
*呼び出し側は@State
 +
<pre>
 +
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"))
 +
                :
 +
}
 +
        }
 +
    }
 +
}
 +
</pre>
 +
=====@Binding使用時のPreview=====
 +
*@Stateかつ、staticで宣言
 +
*$で変数を渡す
 +
<pre>
 +
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)
 +
    }
 +
}
 +
</pre>
 +
 +
==メニュー==
 +
===メインメニューに別のViewを開くメニューを追加===
 +
----
 +
[[File:swift_ui_main_menu.png|400px]]
 +
*.commandsを記述
 +
<pre>
 +
@main
 +
struct WoLApp: App {
 +
    var body: some Scene {
 +
        WindowGroup {
 +
            ContentView()
 +
        }.commands {
 +
            CommandGroup(after: CommandGroupPlacement.appInfo) {
 +
                Divider()
 +
                NavigationLink(destination: PreferenceView()) {
 +
                    Text("preferences")
 +
                }
 +
            }
 +
        }
 +
    }
 +
}
 +
</pre>
 +
 +
==図形==
 +
===Capsule===
 +
----
 +
[[File:swiftui_capsule.png|200px]]
 +
 +
<pre>
 +
VStack{
 +
  :
 +
}
 +
.padding()
 +
.background(
 +
Capsule(style: .continuous)
 +
.foregroundColor(Color.white)
 +
)
 +
.shadow(radius:10 )
 +
</pre>
 +
 +
===RoundedRectangle===
 +
----
 +
[[File:swiftui_roundedRectangle.png|200px]]
 +
<pre>
 +
VStack{
 +
  :
 +
}
 +
.padding()
 +
.background(
 +
RoundedRectangle(cornerRadius: 20)
 +
.foregroundColor(Color.white)
 +
)
 +
.shadow(radius:10 )
 +
</pre>
 +
 +
==画像==
 +
===SF Symbol アイコン===
 +
----
 +
*https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/
 +
*[https://developer.apple.com/sf-symbols/ SF Symbols アプリ]
  
 
==コードサンプル(コンポーネント)==
 
==コードサンプル(コンポーネント)==
179行目: 538行目:
 
}
 
}
 
</pre>
 
</pre>
 +
====Tab切り替えイベント====
 +
----
 +
<pre>
 +
    @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)")
 +
        }
 +
    }
 +
</pre>
 +
 
===Binding===
 
===Binding===
 
----
 
----
211行目: 589行目:
 
         .frame(width: 40)
 
         .frame(width: 40)
 
      
 
      
 +
    }
 +
}
 +
</pre>
 +
====リストのBinding====
 +
----
 +
[[File:swiftui_binding_list.png|500px]]
 +
 +
*Model
 +
<pre>
 +
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] = []
 +
}
 +
</pre>
 +
*UI
 +
**各所(①②③)に$をつける
 +
<pre>
 +
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")
 
     }
 
     }
 
}
 
}
313行目: 758行目:
 
</pre>
 
</pre>
  
===List(繰り返し、Section)===
+
====List(繰り返し、Section)====
 
----
 
----
 
[[File:Swiftui_list_section.png|600px]]
 
[[File:Swiftui_list_section.png|600px]]
349行目: 794行目:
 
}
 
}
 
</pre>
 
</pre>
===Listにオブジェクトを表示===
+
 
 +
====Listにオブジェクトを表示====
 
----
 
----
 
[[File:swift_ui_object_load_to_list.png|500px]]
 
[[File:swift_ui_object_load_to_list.png|500px]]
388行目: 834行目:
 
     var ip: String = ""
 
     var ip: String = ""
 
     var macaddr: String = ""
 
     var macaddr: String = ""
 +
}
 +
</pre>
 +
===Table===
 +
----
 +
*https://developer.apple.com/documentation/swiftui/table
 +
 +
[[File:swiftui_tableview.png|500px]]
 +
 +
*Service
 +
<pre>
 +
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)
 +
        }
 +
    }
 +
}
 +
</pre>
 +
*View
 +
<pre>
 +
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")
 +
 +
    }
 
}
 
}
 
</pre>
 
</pre>
461行目: 1,007行目:
 
         .onReceive(currentTimer) { date in
 
         .onReceive(currentTimer) { date in
 
             self.now = date
 
             self.now = date
 +
        }
 +
    }
 +
}
 +
</pre>
 +
===動的に検索===
 +
[[File:swiftui_dynamic_search.png|400px]]
 +
 +
<pre>
 +
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)
 +
                }
 +
            }
 
         }
 
         }
 
     }
 
     }
480行目: 1,064行目:
 
</pre>
 
</pre>
  
===Tips===
+
==Tips==
 +
----
 +
===[https://www.typea.info/blog/index.php/2021/11/30/swift_macos_app_permission/ ファイルパーミッションエラー(App Sandbox)]===
 +
----
 +
*[https://www.typea.info/blog/index.php/2021/11/30/swift_macos_app_permission/ ファイルパーミッションエラー(App Sandbox)]
 +
===[https://www.typea.info/blog/index.php/2021/01/23/swiftui_tips_view_component_locate/ 画面部品の追加方法]===
 +
----
 +
 
 +
===SwiftUIライブラリ===
 +
----
 +
====[https://github.com/SwiftUIX/SwiftUIX SwiftUIX]====
 +
[https://qiita.com/yosshi4486/items/3d92f81feaabc1049b4c SwiftUIアプリケーション開発の不足を補うSwiftUIX]
 +
 
 +
===ファイル選択===
 
----
 
----
====[https://www.typea.info/blog/index.php/2021/01/23/swiftui_tips_view_component_locate/ 画面部品の追加方法]====
+
<pre>
 +
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
 +
}
 +
</pre>

2024年8月14日 (水) 07:37時点における最新版

| 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 ナビゲーター


Xcode navgator icons.png 左から

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

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

Custom Image Viewの作成

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

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

レイアウト

Alignment


余白の取り方


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

データ

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

  • 画面の構造体の中でデータを保持しているクラスのインスタンスを格納するプロパティに、@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の表示フラグを共有

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

リストのBinding


Swiftui binding list.png

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

画像


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

Table


Swiftui tableview.png

  • 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の中で宣言する

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


ファイルパーミッションエラー(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
}