SwiftUI
| Swift | Mac | Xcode | Swift Sample |
SwiftUI
- SwiftUI
- SwiftUI Documents
- macos tutorials
- 1セットのツールとAPIを使用するだけで、あらゆるAppleデバイス向けのユーザーインターフェイスを構築
- 宣言型シンタックスを使
- 宣言型のスタイルは、アニメーションなどの複雑な概念にも適用
デザインツール
- Xcodeには、SwiftUIでのインターフェイス構築をドラッグ&ドロップのように簡単に行える直感的な新しいデザインツールが含まれています
- デザインキャンバスでの編集内容と、隣接するエディタ内のコードはすべて完全に同期されます
ドラッグ&ドロップ
- ユーザーインターフェイス内のコンポーネントの位置は、キャンバス上でコントロールをドラッグするだけで調整できます
ダイナミックリプレースメント
- wiftのコンパイラとランタイムはXcode全体に完全に埋め込まれているため、Appは常にビルドされ実行されます
- 表示されるデザインキャンバスは、単にユーザーインターフェイスに似せたものではなく、実際のAppそのもの
- Xcodeは編集したコードを実際のAppに直接組み入れることができます
プレビュー
- プレビューを1つまたは複数作成して、サンプルデータを取得できる
Swift UI チュートリアルをやってみる
プロジェクト作成〜TextViewのカスタマイズ
Custom Image Viewの作成
Xcodeを使ってmacOS プログラミングとplaygroundの作成
Listとナビゲーションとプレビュー
レイアウト
余白の取り方
- 余白の取り方
- 記述箇所によって、表示が変わる
- backgroundにpaddingを指定する(cssのmargin的な効果)
- 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のサイズ情報を取得する
- https://qiita.com/masa7351/items/0567969f93cc88d714ac
- https://www.hackingwithswift.com/quick-start/swiftui/how-to-make-two-views-the-same-width-or-height
- 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
- Card View
- LazyGrid
- 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)
- }
- }
- }
データ
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間データ受け渡し
- https://qiita.com/noby111/items/26405bd89075c841029a
- ObservableObject を経由して親子ViewでAlertの表示フラグを共有する
- 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
- }
メニュー
メインメニューに別の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 )
画像
SF Symbol アイコン
コードサンプル(コンポーネント)
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)
- }
- }
- }
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)
- }
- }
画像
- 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 = ""
- }
コードサンプル(ロジック)
Observable(@ObservedObject,@Published,@State)
- データクラスはObservableObjectプロトコル準拠とする。
- 監視対象とするプロパティに@Published属性を付加する。
- データクラスのインスタンスは@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
- }
- }
- }
バックグラウンドからUIを操作する
- observableobj が、ObservableObject の派生クラス
- contentフィールドに、@Published アノテーション
- Viewで、@ObservedObjectを付与しインスタンスを生成
- 上記で、バックグラウンドから、observableobj.contentを操作すると、UIはメインスレッドから触るように怒られる。
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
- DispatchQueue.main.syncで囲む
- DispatchQueue.main.sync {
- observableobj.content = text
- }
Tips
画面部品の追加方法
SwiftUIライブラリ
SwiftUIX
© 2006 矢木浩人