| [[Swift]] | [[Mac]] | [[Xcode]] | [[Swift Sample]] | [[Cocoa]] | [[Xamarin.Mac]] |
{{amazon|B082SMJC7V}}
===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>
@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 アプリ]
==コードサンプル(コンポーネント)==
}
</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===
----
.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")
}
}
</pre>
====List(繰り返し、Section)====
----
[[File:Swiftui_list_section.png|600px]]
}
</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 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>
==コードサンプル(ロジック)==
===Observable(@ObservedObject,@Published,@State)===
----
#データクラスはObservableObjectプロトコル準拠とする。
#監視対象とするプロパティに@Published属性を付加する。
#<u>データクラスのインスタンスは@ObservedObject属性を付加してViewの中で宣言する</u>
[[File:swift_sample_observable.png|200px]]
*Publish
.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>