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

MyMemoWiki

差分

ナビゲーションに移動 検索に移動
22,236 バイト追加 、 2022年3月9日 (水) 15:50
編集の要約なし
| [[Swift]] | [[Mac]] | [[Xcode]] | [[Swift Sample]] | [[Cocoa]] | [[Xamarin.Mac]] |
{{amazon|B082SMJC7V}}
==SwiftUI==
*[https://developer.apple.com/jp/xcode/swiftui/SwiftUI]*[https://developer.apple.com/documentation/swiftui/ SwiftUI Documents]*[https://www.raywenderlich.com/macos macos tutorials]
*1セットのツールとAPIを使用するだけで、あらゆるAppleデバイス向けのユーザーインターフェイスを構築
*宣言型シンタックスを使
===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/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/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> ==データ===== 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間データ受け渡し===----*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>@mainstruct 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 アプリ] ==コードサンプル(コンポーネント)==
===Button===
----
}
</pre>
===Toggle(@State)===
----
[[File:swift_sample_toggle.png|200px]]
).frame(width: 200)
}
}
}
</pre>
 
===Alert===
----
[[File:swift_sample_alert.png|200px]]
<pre>
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: {}))
})
}
}
</pre>
 
===Tab===
----
[[File:swift_sample_tab.png|200px]]
<pre>
 
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)
}
}
}
</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===
----
[[File:swift_sample_binding.png|200px]]
<pre>
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)
}
}
</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")
}
}
===図形===
----
[[File:Swiftui_sample_figure.png|600px]]
<pre>
===List===
----
[[File:Swiftui_list_sample.png|600px]]
<pre>
}
</pre>
 ====List(繰り返し、Section)====----
[[File:Swiftui_list_section.png|600px]]
<pre>
</pre>
====Listにオブジェクトを表示====----[[File:swift_ui_object_load_to_list.png|500px]]<pre>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) } } }}</pre>*<pre>import Foundation class HostList : ObservableObject { @Published var hosts: [Host] = []} class Host { var host: String = "" var ip: String = "" var macaddr: String = ""}</pre>===Table===----*https://developer.apple.com/documentation/swiftui/table [[File:swiftui_tableview.png|500px]] *Service<pre>import Foundationimport AppKitimport 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> ==コードサンプル(ロジック)== ===Observable(@ObservedObject,@Published,@State)===----#データクラスはObservableObjectプロトコル準拠とする。#監視対象とするプロパティに@Published属性を付加する。#<u>データクラスのインスタンスは@ObservedObject属性を付加してViewの中で宣言する</u>[[File:swift_sample_observable.png|200px]]*Publish<pre>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 }}</pre>*Subscribe<pre>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 } }}</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) } } } }}</pre> ===バックグラウンドからUIを操作する===----*observableobj が、ObservableObject の派生クラス*contentフィールドに、@Published アノテーション*Viewで、@ObservedObjectを付与しインスタンスを生成*上記で、バックグラウンドから、observableobj.contentを操作すると、UIはメインスレッドから触るように怒られる。<blockquote>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.</blockquote>*DispatchQueue.main.syncで囲む<pre>DispatchQueue.main.sync { observableobj.content = text}</pre> ==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] ===ファイル選択===----<pre> let dialog = NSOpenPanel(); dialog.title = "Choose a file"dialog.showsResizeIndicator = truedialog.showsHiddenFiles = falsedialog.allowsMultipleSelection = falsedialog.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>

案内メニュー