【SwiftUI】Search TextField を作る

これ。

NavigationStack なしでも、

自在にどこでも置きたいのだが。

👉 SwiftUIでSearchBarが使いたいので自作する | DevelopersIO hatena-bookmark

私も作ってみます、

リストが表示されるやつ。

とりあえず、横に並べて開始します。


HStack {
  Button {
  } label : {
    Image(systemName: "magnifyingglass")
  }

  TextField("Search", text: .constant(""))

  Button {
  } label : {
    Image(systemName: "xmark.circle.fill")
  }
}

 

🧑🏻‍💻 フォーカスの位置の付け替え

デフォルトの TextField のフォーカスの状態を取得して、

エフェクトは無効化する。


TextField("Search", text: .constant(""))
  .focused($focused) 
  .textFieldStyle(.plain)

それらの外にある HStack の border をフォーカス状態によって色を切り替える。


}  // HStack
.border(focused ? Color.accentColor : .gray, width: 1)

 

🧑🏻‍💻 リストのオーバーラップ表示

フォーカスの状態によって、

選択肢のリスト表示を overlay します。

offset 値は入力エリアの高さです。


}  // HStack
.border(focused ? Color.accentColor : .gray, width: 1)
.overlay {
  if focused {
    VStack {
      ForEach(0...3, id: \.self) { i in
        HStack {
          Text("\(i)")
            Spacer()
        }
      }
    }
    .background()
    .offset(y: -45)
  }
}

あとは、レイアウトを調整していけば OK!



 

🧑🏻‍💻 フォーカスのアニメーション

よく見てみると、フォーカスってアニメーションだったんですか !

フォーカス処理部分をバックグラウンドでアニメ化しました。


.background {
  RoundedRectangle(cornerRadius: 8)
    .stroke(.gray, lineWidth: 0.5)
  RoundedRectangle(cornerRadius: 8)
    .stroke(
      focused ? .orange : .clear,
      lineWidth: focused ? 5 : 16
    )
}
.animation(.default, value: focused)

 

🧑🏻‍💻 まとめ



もうググってもしんどいですね。

ChatGPT や copilot などのAI系でも時間がかかるし。

👉 【SwiftUI】#Preview with @Binding arguments hatena-bookmark



【SwiftUI】View のロジック部分を外出しにする構成

ModelView を作成することで

「ロジック部分を外出し」

して View の見通しを良くすることができます。

同時に @State も外部クラスに任せますか。


@Observable final class StateHolder {
  var text = ""
}


var stateHolder = StateHolder()

TextField("text", text: Binding(
  get: { stateHolder.text },
  set: { newValue in stateHolder.text = newValue })
)


var stateHolder = StateHolder()

@Bindable var sh = stateHolder
TextField("text", text: $sh.text)

複雑なデータ処理ロジック部分も置いてしまうと、

実質の今どきの ModelView となりますね。

基本、ModelView は View に対して 1:1 なことを忘れないでください。



👉 【SwiftUI】@State と 単方向データフロー hatena-bookmark
👉 【SwiftUI】ModelView は 単なる StateHolder なのか ? - ModelView の役割 hatena-bookmark


【Swift】ログイン時に起動する - SMAppService


import Foundation
import ServiceManagement
import os.log

@available(macOS 13.0, *)
public enum LaunchOnLogin {
  public static var isEnabled: Bool {
    get { SMAppService.mainApp.status == .enabled }
    set {
      do {
        if newValue {
          if SMAppService.mainApp.status == .enabled {
            try? SMAppService.mainApp.unregister()
          }
          try SMAppService.mainApp.register()
        } else {
          try SMAppService.mainApp.unregister()
        }
      } catch {
        os_log("Failed to \(newValue ? "enable" : "disable") launch at login: \(error.localizedDescription)")
      }
    }
  }
}

👉 Rectangle/Rectangle/LaunchOnLogin.swift at 277cadc905d9ff92c7948a5c97794141f8d1277c · rxhanson/Rectangle hatena-bookmark


import AppKit
import ServiceManagement

@available(macOS 13.0, *)
class LoginItemManager {
  func loginItemIsEnabled() -> Bool {
    return SMAppService.mainApp.status == .enabled
  }

  func disableLoginItem() {
    try? SMAppService.mainApp.unregister()
  }

  func enableLoginItem() {
    try? SMAppService.mainApp.register()
  }
}

👉 phpmon/phpmon/Common/Helpers/LoginItemManager.swift at 4a3dee3c5023ef252e5dd48de4d2937087eb941b · nicoverbruggen/phpmon hatena-bookmark


import ServiceManagement

public extension SMAppService {
  var isEnabled: Bool {
    status == .enabled
  }

  func toggle() throws {
    isEnabled ? try unregister() : try register()
  }
}

👉 LunarBar/LunarBarMac/Modules/Sources/AppKitExtensions/SMAppService+Extension.swift at e7ae92a8fca7895d739686839afa450fd60663de · LunarBar-app/LunarBar hatena-bookmark

 

■ まとめ

以上を参考に、こう書きました。


@AppStorage("launchAtLogin") private var isOn = false

Toggle("Launch at login", isOn: $isOn)
  .onAppear {
    isOn = SMAppService.mainApp.status == .enabled
  }
  .onChange(of: isOn) {
    let app = SMAppService.mainApp
    try? isOn ? app.register() : app.unregister()
  }

その後、結局、

@AppStorage も @State も持たないようにしました。



登録状態は、


[Settings] - [General] - [Login Items]

でOS側設定画面からも管理できます。

しかし、双方向のバインディングはつらくないですか。

👉 SMAppService | Apple Developer Documentation hatena-bookmark