- 投稿日:2020-03-24T22:55:27+09:00
デフォルトTableViewサンプル集 【操作編】(swift)
はじめに
デフォルトTableViewサンプル集(swift)これのセル選択や編集などの操作編です。
複数選択 複数選択 2 一部選択不可 セル編集
メニュー インデックスバー スワイプ 複数選択
セルの複数選択処理です。
こんな感じ。下記のように設定
tableView.allowsMultipleSelection = true処理としては上記のようにフラグを
true
にするだけでできる。選択したものにチェックマークをつけたい場合は下記も実装。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) // ここ cell.accessoryType = tableView.indexPathsForSelectedRows?.contains(indexPath) == true ? .checkmark : .none cell.selectionStyle = .none // 選択時に背景色を変えないため return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if let cell = tableView.cellForRow(at: indexPath) { cell.accessoryType = .checkmark // チェックつける } } func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { if let cell = tableView.cellForRow(at: indexPath) { cell.accessoryType = .none // チェックはずす } }パターン2(20200325追記)
そういえば左側にチェックマーク置くのも簡単にできたので追記です。
こんな感じ。
実装は下記3行を追加するだけ!
tableView.isEditing = true tableView.allowsMultipleSelection = true tableView.allowsMultipleSelectionDuringEditing = true一部のセル選択不可
一部のセル選択不可にしたい場合は下記のように設定します。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) // 選択不可にしたい場合にnone設定 cell.selectionStyle = selectable ? .default : .none return cell } func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { // cell.selectionStyle = .noneだけだとselect自体はできる // 選択不可の場合にnilを設定する return selectable ? indexPath : nil }
cell.selectionStyle = .none
だけだとセルタップ時にtableView(_:didSelectRowAt:)
は呼ばれるしindexPathForSelectedRow
にも値が設定されてしまうのでtableView(_:willSelectRowAt:)
も設定する。セルの編集
初期表示で編集モードにしたい場合は
tableView.isEditing = true
を設定する。編集ボタンを表示したい場合は
navigationItem.rightBarButtonItem = editButtonItem
でいける!編集モードの場合は完了ボタンに自動で変わってくれるUITableViewController
の場合は編集ボタン押下で自動で編集モードに変わってくれる(UIViewController
の場合はsetEditing(_ editing: Bool, animated: Bool)
を実装してtableView.isEditing = true
で設定してやる必要がある)編集モードの場合もセル選択を有効にしたい場合は
tableView.allowsSelectionDuringEditing = true
を設定する。こんな感じ
削除
削除ボタンを表示する。
下記を設定する。
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { tableDataList.remove(at: indexPath.row) // 先にリストから削除する // テーブルの高さが変わるのでこれで囲む tableView.beginUpdates() tableView.deleteRows(at: [indexPath], with: .automatic) tableView.endUpdates() } }これで削除ボタンタップ時と左にスワイプした時に削除処理がはしる。
追加
追加ボタンを表示する。
下記を設定する。
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .insert { // 対象セルの下に追加(先にリストに追加する) tableDataList.insert(0, at: indexPath.row + 1) tableView.beginUpdates() tableView.insertRows(at: [IndexPath(row: indexPath.row + 1, section: 0)], with: .automatic) tableView.endUpdates() } } override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { return .insert // これを設定しないと削除モードになる }移動
セルを移動させる。
下記を設定する。
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { return true } override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { // 移動処理 let element = tableDataList[sourceIndexPath.row] tableDataList.remove(at: sourceIndexPath.row) tableDataList.insert(element, at: destinationIndexPath.row) }削除ボタンを表示したくない場合
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { return .none }編集モード時にインデントを変更したくない場合(削除or追加ボタンがある場合は変わらない)
override func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool { return false }移動不可
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { return indexPath.row != 0 // 一番上のセルを移動不可に設定 }上記の設定で一番上のセル(row=0)は移動不可になりましたが他のセルを一番上に移動させることは可能です移動先としても不可にしたい場合は下記も設定する。
override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath { return indexPath.row != 0 ? sourceIndexPath : proposedDestinationIndexPath }ロングタップでメニュー表示
こんな感じのやつすぐ実装できんねや!知らんかった!と思ったら全部 iOS13 で Deprecated だった...
func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?)代わりにこれを使うらしい
tableView(_:contextMenuConfigurationForRowAt:point:)
こんな感じ
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { actions -> UIMenu? in let copy = UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc"), identifier: nil) { action in if let cell = tableView.cellForRow(at: indexPath) { UIPasteboard.general.string = cell.textLabel?.text } } return UIMenu(__title: "Menu", image: nil, identifier: nil, children: [copy]) } return configuration }インデックスバー
標準の電話帳アプリとかにあるやつ。
こんな感じ
テーブルの style が
.grouped
でも表示されるがドキュメントには.plain
のみみたいなことが書いてあるので.plain
にしましょう。The table view must be in the plain style (UITableViewStylePlain).
参考:ドキュメント
func sectionIndexTitles(for tableView: UITableView) -> [String]? { return ["A", "B", "C"] // インデックスバーの文字設定 } // 特殊なことをしないのであれば特に実装しなくても対象のセクションが表示される func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { return index }虫眼鏡アイコン下記のようにすると一番上に虫眼鏡アイコンが表示される。
func sectionIndexTitles(for tableView: UITableView) -> [String]? { return [UITableView.indexSearch, "A", "B", "C"] }使うなら一番上に置けみたいなことが書いてある。
This location should generally be the first title in the index.
参考:ドキュメント
これの使い道はよくわかりません。。。
カスタム
下記のように色が設定できる。
tableView.sectionIndexColor = .white // 文字色 tableView.sectionIndexBackgroundColor = .systemPurple // バーの背景色 tableView.sectionIndexTrackingBackgroundColor = .systemRed // ハイライト時の背景色スワイプ
なんか昔ライブラリ入れてやった気がしますが標準でスワイプアクションがあるそうです。
こんな感じ。
(めっちゃアクションを追加できるけどたぶんそんな使い方をすることはない)
下記のように実装する。
// 右へスワイプ func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let action = UIContextualAction(style: .normal, title: "Action") { (action, view, completionHandler) in // なんか処理 completionHandler(true) // 処理成功時はtrue/失敗時はfalseを設定する } let configuration = UISwipeActionsConfiguration(actions: [action]) return configuration } // 左へスワイプ func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let action = UIContextualAction(style: .destructive, title: "Action") { (action, view, completionHandler) in // なんか処理 completionHandler(true) // 処理成功時はtrue/失敗時はfalseを設定する } let configuration = UISwipeActionsConfiguration(actions: [action]) return configuration }Action の
style
にはnormal
とdestructive
(削除系)の2種類があります。スワイプを引っ張り切ると
UISwipeActionsConfiguration(actions: [action])
の最初のaction
が実行されます。これを無効にした場合は
configuration.performsFirstActionWithFullSwipe = false
を設定する。カスタム
action.backgroundColor = .systemBlue // 背景色設定 action.image = UIImage(systemName: "pencil.circle.fill") // 画像設定(タイトルは非表示になる)さいごに
Table関連は頻繁に処理が追加されてるのでたまにチェックした方がいい。追加されたときは見るけど古いバージョンも動作保証してると使えないのでよく忘れます...たまにチェックしよう!
ドラッグドロップは色々できそうなのでまた時間があれば追加します...
プルリクくれてもええんやで
- 投稿日:2020-03-24T20:25:34+09:00
swiftでキーボードの扱いが面倒なので「IQKeyboardManager」を使ったら神だった
swiftでテキストを入力したりするときにキーボードの操作もしなければいけないのがすごく面倒くさい。
何てったって
・キーボードを表示
・doneをクリックしたらキーボードを隠すなのですが、これがよく忘れたりきちんと動かなかったり(俺が悪い)、ビルドしてから気づく。なんてことがあったので探してみたら速攻見つかりました。
使用感
簡単に使ってみましたがすごくいい。
https://www.youtube.com/watch?time_continue=4&v=iS1v7tSPUbc&feature=emb_title
参考
https://qiita.com/k-yamada-github/items/d4c791424ead414e3142
- 投稿日:2020-03-24T19:20:43+09:00
CombineによるPublisherの受け渡し
はじめに
RxSwift
を利用したVIPER
アーキテクチャのソースコードをcombine
で書き直している。
RxSwift
では、Interactor
がAPIから非同期処理で取得したデータをPresenter
に渡す際、Single
オブジェクトとして扱っていた。Single
だったところをcombine
ではFuture
オブジェクトにしたのだが、両者はGenerics
の型の数が異なる。直接参考になるソースを見つけられなかったため我流のものだが書き残しておく。ただ、もっと良い方法があると思われるので、詳しい方にご教示いただきたいというのが正直なところである。ソースコード
fetchFutureOnPresenter(flag: Bool)
にtrue
を入れればString
がInt
に変換された値が返ってくる。false
にすればPresenter
を経由せずにエラーが投げられることをplaygroundで動作確認をした。struct APIError: Error { var description: String } // InteractorがAPIにアクセスしてレスポンスを取得する想定 func fetchFutureOnInteractor(flag: Bool) -> AnyPublisher<String, Error> { return Future<String, Error>{ promise in /// /// 実際はここに非同期処理を書く想定 /// // 非同期処理の結果によって、場合分け if flag { promise(.success("0")) } else { promise(.failure(APIError(description: "☓"))) } } .eraseToAnyPublisher() } // PresenterがInteractorのメソッドから取得したデータをViewに渡す用に変換する想定 // flatMapでAnyPublisher<String, Error>からAnyPublisher<Int, Error>に変換 func fetchFutureOnPresenter(flag: Bool) -> AnyPublisher<Int, Error> { return fetchFutureOnInteractor(flag: flag) .flatMap { output -> AnyPublisher<Int, Error> in // クロージャの戻り値をIntにする Future<Int, Error>{ promise in promise(.success(Int(output)!)) // Intにキャスト、エラー(Failure)は、ここでは記載しない } .eraseToAnyPublisher() } .eraseToAnyPublisher() } // Presenterが購読(.sink) let cancellable = fetchFutureOnPresenter(flag: /* true or false */) .sink(receiveCompletion: { completion in switch completion { case .finished: break case .failure(let error): // Interactorの.failureはここに投げられる print("error \(error)") } }, receiveValue: { value in // このvalueを元にViewが参照する値を更新する想定 print("value \(value)") })
- 投稿日:2020-03-24T18:44:20+09:00
SwiftUIのPreview実装をそのまま使って、Screenshot撮影を自動化する
はじめに
SwiftUIのPreview機能を活用して、実装したPreview用のコードをそのままScreenshot撮影自動化に利用できる方法の紹介です。
わざわざテスト用のコードを実装することなく、既存の記述をほぼそのままCIとして組み込めるので、かなり簡単に対応できると思います。
これを使って、画像diffから意図しないUI変更を検知したり、簡単なUIカタログのようなものを作ることもできます。完成形
先に完成形を記載しておきます
Previewの実装
struct ContentView_Previews: PreviewProvider { // Previewableに適合したenumを定義して、previewプロパティで状態に応じたViewを返すだけです enum Context: String, Previewable { case red case green case blue var preview: some View { switch self { case .red: return ContentView() .foregroundColor(.red) case .green: return ContentView() .foregroundColor(.green) case .blue: return ContentView() .foregroundColor(.blue) } } } static var previews: some View { // Previewableに定義してある便利関数で、各caseごとのViewをGroupで括った上で返却します Context.groupedAllContext } }Preview画面での実際の表示はこんな感じ
テストコードの実装
テスト用の実装はこれだけでOKです
class PreviewScreenshotSampleTests: FBSnapshotTestCase { override func setUp() { super.setUp() // ここは、今回利用したFBSnapshotTestCaseのための実装です self.recordMode = true } func test() { // Previewableに適合した型の、screenshotメソッドを呼び出すだけです。 ContentView_Previews.Context.screenshot(self) } }画像はこんな感じで出力されます
Previewで表示されていたものと同じ画面が撮影できているのがわかると思います。上記の通り、
Previewable
に適合した型のpreview
プロパティから、スクリーンショットを撮影したい状態のViewを返してあげるだけで、SwiftUIのPreviewとUnitTest両方から同じ状態の画面を参照することができました。以下では仕組みの方の解説をします。
ライブラリの準備
今回は ios-snapshot-test-case を利用してスクリーンショットを撮影しました。
特にライブラリに依存した実装はないので、お好みのライブラリでスクリーンショットを撮影できるはずです。ios-snapshot-test-case用の設定としてSchemeに以下の環境変数を設定します。
これにより、指定したディレクトリに画像が保存されます。
Preiewableの定義
実装はこれだけです。詳細はコメントに記述しています。
protocol Previewable: CaseIterable, Hashable, RawRepresentable { associatedtype Preview: View var preview: Preview { get } } // Groupを使ってPreiewを分けたり、Stackを使って縦に並べるための便利関数です。 // 無くても問題ありません extension Previewable where Self.AllCases: RandomAccessCollection, Self.RawValue == String { static var groupedAllContext: some View { Group { ForEach(Self.allCases, id: \.self) { // Stringのenumで定義しておけば、case名がそのままPreiew内に表示されるようにしています $0.preview.previewDisplayName($0.rawValue) } } } static var stackedAllContext: some View { VStack { ForEach(Self.allCases, id: \.self) { $0.preview.previewDisplayName($0.rawValue) } } } }extension Previewable where Self.AllCases: RandomAccessCollection, Self.RawValue == String { static func screenshot(_ testCase: FBSnapshotTestCase) { // 定義されてある全てのケースを1つずつ撮影していく Self.allCases.forEach { (ctx) in ctx.screenshot(for: testCase) } } func screenshot(_ testCase: FBSnapshotTestCase) { let window = UIWindow(frame: UIScreen.main.bounds) // previewプロパティから対象の状態のViewを生成します window.rootViewController = UIHostingController(rootView: self.preview) window.makeKeyAndVisible() let expectation = testCase.expectation(description: "") DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { // ここで任意のスクリーンショット撮影処理を実行します。 // 今回はFBSnapshotTestCaseのメソッドを使うため、画面に表示されたぐらいのタイミングで、UIViewの型でSwiftUIの画面を取り出して受け渡します // identifierにrawValueを渡すことで、出力されるファイルの名前を定義したcaseの名前になるようにしています testCase.FBSnapshotVerifyView(window.rootViewController!.view, identifier: self.rawValue) expectation.fulfill() } testCase.wait(for: [expectation], timeout: 5.0) } }以上で必要な実装は終わりです。
サンプルコード
- 投稿日:2020-03-24T16:56:17+09:00
AuthKeyを利用してコマンド(curl)でPushテストする
メモ書き
簡単にPushテストできる方法をメモしておきます。
下記のスクリプト叩くだけ
/bin/bash push_test.shdeviceTokenはこんな感じで事前に取っておく。
AppDelegate.swiftfunc application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { var token: String = "" for i in 0..<deviceToken.count { token += String(format: "%02.2hhx", deviceToken[i] as CVarArg) } print("push token :\(token)") } }deviceTokenとAuthKey(デベロッパーページで作成)は事前に用意する。
push_test.sh#!/bin/bash deviceToken=dbfacf621430827771306fadb878304e9fd956f509527fc55bfd216915dxxx authKey="./AuthKey_XXXX43J85J.p8" authKeyId=XXXX43J85J teamId=XXXX798F94 # distribution # bundleId=jp.co.archive-asia.demo # endpoint=https://api.push.apple.com # development bundleId=jp.co.archive-asia.demo endpoint=https://api.development.push.apple.com #テスト read -r -d '' payload <<-'EOF' { "aps": { "badge": 2, "category": "あああ", "alert": { "title": "タイトル", "subtitle": "サブタイトル", "body": "ホゲホゲ" } }, "custom": { "mykey": "カスタムデータ" } } EOF base64() { openssl base64 -e -A | tr -- '+/' '-_' | tr -d = } sign() { printf "$1"| openssl dgst -binary -sha256 -sign "$authKey" | base64 } time=$(date +%s) header=$(printf '{ "alg": "ES256", "kid": "%s" }' "$authKeyId" | base64) claims=$(printf '{ "iss": "%s", "iat": %d }' "$teamId" "$time" | base64) jwt="$header.$claims.$(sign $header.$claims)" curl --verbose \ --header "content-type: application/json" \ --header "authorization: bearer $jwt" \ --header "apns-topic: $bundleId" \ --data "$payload" \ $endpoint/3/device/$deviceToken
- 投稿日:2020-03-24T16:56:17+09:00
コマンド(curl)でPushテストする方法(iOS)
メモ書き
AuthKeyを利用して簡単にPushテストできるのが分かったのでメモしておきます。
事前作業
- deviceTokenはこんな感じで事前に取っておく。
AppDelegate.swiftfunc application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { var token: String = "" for i in 0..<deviceToken.count { token += String(format: "%02.2hhx", deviceToken[i] as CVarArg) } print("push token :\(token)") } }
- AuthKey(デベロッパーページで作成)を取得
スクリプト
push_test.sh#!/bin/bash deviceToken=dbfacf621430827771306fadb878304e9fd956f509527fc55bfd216915dxxx authKey="./AuthKey_XXXX43J85J.p8" authKeyId=XXXX43J85J teamId=XXXX798F94 # distribution # bundleId=jp.co.archive-asia.demo # endpoint=https://api.push.apple.com # development bundleId=jp.co.archive-asia.demo endpoint=https://api.development.push.apple.com #テスト read -r -d '' payload <<-'EOF' { "aps": { "badge": 2, "category": "あああ", "alert": { "title": "タイトル", "subtitle": "サブタイトル", "body": "ホゲホゲ" } }, "custom": { "mykey": "カスタムデータ" } } EOF base64() { openssl base64 -e -A | tr -- '+/' '-_' | tr -d = } sign() { printf "$1"| openssl dgst -binary -sha256 -sign "$authKey" | base64 } time=$(date +%s) header=$(printf '{ "alg": "ES256", "kid": "%s" }' "$authKeyId" | base64) claims=$(printf '{ "iss": "%s", "iat": %d }' "$teamId" "$time" | base64) jwt="$header.$claims.$(sign $header.$claims)" curl --verbose \ --header "content-type: application/json" \ --header "authorization: bearer $jwt" \ --header "apns-topic: $bundleId" \ --data "$payload" \ $endpoint/3/device/$deviceToken実行
スクリプト叩くだけ
/bin/bash push_test.sh
- 投稿日:2020-03-24T16:06:38+09:00
Swift:マルチディスプレイでもNSStatusBarButtonのアイコンを色を保つ
メニューバーアプリ開発のTipsです.
マルチディスプレイの状態でMacのメニューバーを眺めていると,非アクティブなディスプレイでは基本的にアイコンが色を失ってグレーアウトするのですが,たまにそのお約束を無視している子がいます.
Siriのアイコンとか,LINEのアイコンとかです.
これがどうやってるのか気になって調べたり検証したところ,やり方そのものは簡単なのですが,情報が全然なかったので記しておきます.
ポイント
- NSStatusBarButton.imageにアイコンを設定するだけだとグレーアウトする
- NSStatusBarButtonにNSViewをaddSubviewしてその中でdrawするとグレーアウトしない
- NSStatusBarButton.imageにアイコンを設定しないとボタンの大きさや位置が定まらない
- NSStatusItem.lengthで幅を設定することもできるけれどあまり賢い方法ではない
解決法
- NSStatusBarButton.imageにアイコンサイズと同じサイズの透明なNSImageを設定する
- 独自NSViewを用意して,アイコンNSImageとNSStatusBarButtonの大きさを渡す
- NSStatusBarButtonに独自NSViewをaddSubViewしてマージンを計算して配置
サンプルソース
AppDelegateimport Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) var button: NSStatusBarButton! func applicationDidFinishLaunching(_ aNotification: Notification) { button = statusItem.button button.image = NSImage(size: NSSize(width: 18.0, height: 18.0)) let icon = NSImage(imageLiteralResourceName: "SampleIcon") let iconView = IconView(icon, button.bounds.size) button.addSubview(iconView) } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } }IconViewimport Cocoa class IconView: NSView { var icon: NSImage? init(_ icon: NSImage, _ size: CGSize) { super.init(frame: NSRect(x: 0.5 * (size.width - 18.0), y: 0.5 * (size.height - 18.0), width: 18.0, height: 18.0)) self.icon = icon } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func draw(_ dirtyRect: NSRect) { icon?.draw(in: NSRect(x: 0.0, y: 0.0, width: 18.0, height: 18.0)) } }サンプルの結果
ちゃんと非アクティブなディスプレイでも色を保っています.
ちなみに,メニューバーの高さ上限は22ポイントですが,22ポイントのアイコンだと窮屈なので,高さは18ポイントが良いです.横幅には制限がないです.(18ポイントということは,最近のMacはRetinaディスプレイなので綺麗に表示するなら2倍の36ピクセルが必要です.)
蛇足
NSStatusBarButton.titleに顔文字を指定しても色がついたままになります(色褪せますが).
- 投稿日:2020-03-24T16:06:38+09:00
Swift:マルチディスプレイでもNSStatusBarButtonのアイコンの色を保つ
メニューバーアプリ開発のTipsです.
マルチディスプレイの状態でMacのメニューバーを眺めていると,非アクティブなディスプレイでは基本的にアイコンが色を失ってグレーアウトするのですが,たまにそのお約束を無視している子がいます.
Siriのアイコンとか,LINEのアイコンとかです.
これがどうやってるのか気になって調べたり検証したところ,やり方そのものは簡単なのですが,情報が全然なかったので記しておきます.
ポイント
- NSStatusBarButton.imageにアイコンを設定するだけだとグレーアウトする
- NSStatusBarButtonにNSViewをaddSubviewしてその中でdrawするとグレーアウトしない
- NSStatusBarButton.imageにアイコンを設定しないとボタンの大きさや位置が定まらない
- NSStatusItem.lengthで幅を設定することもできるけれどあまり賢い方法ではない
解決法
- NSStatusBarButton.imageにアイコンサイズと同じサイズの透明なNSImageを設定する
- 独自NSViewを用意して,アイコンNSImageとNSStatusBarButtonの大きさを渡す
- NSStatusBarButtonに独自NSViewをaddSubViewしてマージンを計算して配置する
サンプルソース
AppDelegateimport Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) var button: NSStatusBarButton! func applicationDidFinishLaunching(_ aNotification: Notification) { button = statusItem.button button.image = NSImage(size: NSSize(width: 18.0, height: 18.0)) let icon = NSImage(imageLiteralResourceName: "SampleIcon") let iconView = IconView(icon, button.bounds.size) button.addSubview(iconView) } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } }IconViewimport Cocoa class IconView: NSView { var icon: NSImage? init(_ icon: NSImage, _ size: CGSize) { super.init(frame: NSRect(x: 0.5 * (size.width - 18.0), y: 0.5 * (size.height - 18.0), width: 18.0, height: 18.0)) self.icon = icon } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func draw(_ dirtyRect: NSRect) { icon?.draw(in: NSRect(x: 0.0, y: 0.0, width: 18.0, height: 18.0)) } }サンプルの結果
ちゃんと非アクティブなディスプレイでも色を保っています.
ちなみに,メニューバーの高さ上限は22ポイントですが,22ポイントのアイコンだと窮屈なので,高さは18ポイントが良いです.横幅には制限がないです.(18ポイントということは,最近のMacはRetinaディスプレイなので綺麗に表示するなら2倍の36ピクセルが必要です.)
蛇足
NSStatusBarButton.titleに顔文字を指定しても色がついたままになります(色褪せますが).
- 投稿日:2020-03-24T15:04:50+09:00
Swift:もしかしたらGPUの状態を取得できるかもしれないコード
RunCatでシステム情報を色々取得してきたので,GPUの状態も取得してみようと思い調べたところ,Objective-Cでやっている先駆者がいたのでSwift化してみました.しかし,自分の開発機のMacBook Proは13インチでGPU積んでないので動作検証できませんでした.
誰か試してみたらコメント欲しいです.
import Cocoa import IOKit class ViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() getGPUPerformance() } func getGPUPerformance() { let dict = IOServiceMatching(kIOAcceleratorClassName) var iterator = io_iterator_t() if IOServiceGetMatchingServices(kIOMasterPortDefault, dict, &iterator) == kIOReturnSuccess { while case let regEntry = IOIteratorNext(iterator), regEntry != 0 { var services: Unmanaged<CFMutableDictionary>? = nil if IORegistryEntryCreateCFProperties(regEntry, &services, kCFAllocatorDefault, 0) != kIOReturnSuccess { IOObjectRelease(regEntry) continue } if let servicesDict = services?.takeRetainedValue() as? [String : AnyObject], let ps = servicesDict["PerformanceStatistics"] as? [String : AnyObject], let gpuCore = ps["GPU Core Utilization"] as? Double, let freeVram = ps["vramFreeBytes"] as? Double, let usedVram = ps["vramUsedBytes"] as? Double { let outpu = String(format: "GPU: %.3f%%, VRAM: %.3f%%", gpuCore / 10000000.0, 100.0 * (usedVram / (usedVram + freeVram))) Swift.print(outpu) } services?.release() IOObjectRelease(regEntry) } IOObjectRelease(iterator) } } }
- 投稿日:2020-03-24T12:10:01+09:00
SwiftからOpenCVを使う
概要
基本的にこの記事を参考にしたら上手くできたが、1つ詰まるところがあったので、そこの解消法について記録しておきます。
<参考記事> iOS(swift)でOpenCVを使うシンプルなサンプル『'opencv2/opencv.hpp' file not found』のエラー
参考記事の
OpenCVSample.mm#import <opencv2/opencv.hpp> #import <opencv2/highgui/ios.h>この部分でfile not foundのエラーが発生した。
General > Build Settings > Framework Search Pathsにて、
$(PROJECT_DIR)をrecursiveで設定したら解消された。
- 投稿日:2020-03-24T03:09:24+09:00
Swift:App GroupsとUserDefaultsとNSKeyedUnarchiverの話
iOSでToday Extensionと母機アプリを連携したいときに,App Groupsを使います.そして,UserDefaultsでデータを共有するのですが,独自クラスのデータを共有するとなると少し厄介でした.
App Groupsの準備
データを共有するターゲットのそれぞれにて下記の作業をします.
- [Signing & Capabilities]より
App Groups
を追加- グループIDを追加する(データを共有する全てのターゲットにて同じグループIDを使用すること).
そして,
UserDefaults
を使うときはUserDefaults.standard
ではなくUserDefaults(suiteName: "グループID")
を使います.なお,グループIDはgroup.
から始まるのが通常らしいです.独自クラスの準備
基本的にはこの記事と同様ですが,
NSCoding
だけでなくNSSecureCoding
も使います.独自クラスのサンプルclass Sample: NSObject, NSCoding, NSSecureCoding { var dataA: Bool var dataB: Double var dataC: String static var supportsSecureCoding: Bool { return true } init(_ dataA: Bool, _ dataB: Double, _ dataC: String) { self.dataA = dataA self.dataB = dataB self.dataC = dataC } required init?(coder: NSCoder) { dataA = coder.decodeBool(forKey: "dataA") dataB = coder.decodeDouble(forKey: "dataB") dataC = coder.decodeObject(of: NSString.self, forKey: "dataC") as String? ?? "" } func encode(with coder: NSCoder) { coder.encode(dataA, forKey: "dataA") coder.encode(NSNumber(value: dataB), forKey: "dataB") coder.encode(dataC as NSString, forKey: "dataC") } }
supportsSecureCoding
をtrueにする必要があります.
ここで,ターゲットごとの初期化の場面(AppDelegateのdidFinishLaunchingとかViewDidLoadとか)でNSKeyedArchiver
とNSKeyedUnarchiver
に独自クラスを登録します.例override func viewDidLoad() { super.viewDidLoad() NSKeyedArchiver.setClassName("Sample", for: Sample.self) NSKeyedUnarchiver.setClass(Sample.self, forClassName: "Sample") }データの取得/保存
let userDefaults = UserDefaults(suiteName: "group.com.hoge.sampleApp")! var sample: Sample { get { guard let data = userDefaults.data(forKey: "sample"), let unarchived = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data), let sample = unarchived as? Sample else { return Sample(false, 0.0, "") } return sample } set(newSample) { let data = try? NSKeyedArchiver.archivedData(withRootObject: newSample, requiringSecureCoding: true) userDefaults.set(data, forKey: "sample") } }このように
get
とset
を定義してしまうとデータの出し入れがしやすいかもしれません.参照
- 投稿日:2020-03-24T01:43:09+09:00
iOS13のautomaticモーダルからfullScreenに横遷移する
こーゆう遷移をする方法を紹介します。
前置き
macOS 10.15.x
Xcode 11.2.x
iOS 13.x画面遷移について、iOS13から通常のモーダル表示(下から表示)が、
ちょっと立体的な.automatic
表示になりました。
.automatic .fullScreen iOS12以前は通常が.fullScreenでしたね。
.fullScreenで表示したい場合は、
ちゃんと指定するようにしなければならなくなりました。let viewController = xxx let navigationController = UINavigationController(rootViewController: viewController) navigationController.modalPresentationStyle = .fullScreen present(navigationController, animated: true, completion: nil)この記事は.automaticの状態から、.fullScreenの状態に、
pushViewController
遷移のように横表示する方法を記載します。方法1(?): pushViewController遷移
-> 現状わかりませんでした
色々調べましたが、同じnavigationControllerで、
modalPresentationStyleを変更する方法は現状見つけられませんでした。もし方法が存在し、やり方を知っている方がいましたら教えてくださいm(_ _)m
方法2: present遷移の横バージョン
present(viewController, animated: true, completion: nil)
の遷移って下からですよね。コレを横から遷移に変更する方法が、方法2です。
横から遷移サンプルコード:
import UIKit final class TestFirstViewController: UIViewController { @IBAction private func clickButton(_ sender: Any) { let storyBoard = UIStoryboard(name: "TestSecond", bundle: nil) let viewController = storyBoard.instantiateViewController(withIdentifier: "TestSecondViewController") let navigationController = UINavigationController(rootViewController: viewController) navigationController.modalPresentationStyle = .fullScreen let transition = CATransition() transition.duration = 0.4 transition.type = .push transition.subtype = .fromRight view.window!.layer.add(transition, forKey: kCATransition) present(navigationController, animated: false, completion: nil) } override func viewDidLoad() { super.viewDidLoad() } }ポイントは、
- CATransitionを利用して、presentのanimationを定義していること
- presentのanimatedはfalseにすること
です。ちなみに
transition.type
とtransition.subtype
を
変更すれば、他にも様々な画面遷移アニメーションを実現できます。また、せっかく.automatic状態から横表示を実現しても、
何も設定していなければ、画面を閉じるアニメーションは下方向になってしまいます。
画面を閉じる動作も、横に設定してあげましょう。横へ画面閉じるサンプルコード:
import UIKit final class TestSecondViewController: UIViewController { @objc private func clickBackBarButtonItem() { let transition = CATransition() transition.duration = 0.4 transition.type = .push transition.subtype = .fromLeft view.window!.layer.add(transition, forKey: kCATransition) dismiss(animated: false, completion: nil) } override func viewDidLoad() { super.viewDidLoad() setupViews() } private func setupViews() { let backBarButtonItem = UIBarButtonItem(title: "戻る", style: .plain, target: self, action: #selector(clickBackBarButtonItem)) navigationItem.setLeftBarButton(backBarButtonItem, animated: true) } }表示時と同じく、dismissのanimatedがfalseなことに注意しましょう。
ちなみに
safariViewControllerを用いた場合は、特に何も指定なく横遷移にできるようです。
let safariViewController = SFSafariViewController(url: URL(string: "https://google.com")!) present(safariViewController, animated: true, completion: nil)まとめ
本当は方法1の方で実現できれば楽なのですが。
今後のアップデートに期待です。参考
https://swallow-incubate.com/archives/blog/20200226/
https://zonneveld.dev/ios-13-viewcontroller-presentation-style-modalpresentationstyle/
- 投稿日:2020-03-24T00:40:20+09:00
MusicアプリのTabBarControllerを再現してみた
iPhone、iPadの標準ミュージックアプリのプレイヤー画面付きのTabBarControlelrを再現してみたので、紹介したいと思います。
※音楽アプリ専用というわけではないのでプレイヤー画面以外の好きな画面を表示することはできますが、この記事の中ではプレイヤー画面と言わせていただきます。完成したもの
https://github.com/HiroteruWatanabe/OverlayTabBarController
ソースを見たい方はこちらからどうぞ! CocoaPodsでも導入可能です。
CocoaPodsの作成に慣れていないので、いろいろ荒いところはありますが、気にしないでください。動作環境
iOS 13.0以上
Swift 5iPhone
iPhoneの場合はタブバーの上にプレイヤーのプレビュー画面が表示されます。
プレビュー画面をタップするとプレイヤーをモーダル表示します。
プレイヤー画面の高さによって表示形式が変わります。
プレイヤー非表示時 プレイヤー表示時 プレイヤー表示時(セミモーダル) iPad
iPadの場合はタブバーの右横にプレビュー画面が表示されます。
プレイヤー非表示時 プレイヤー表示時 使い方
基本的にはプレイヤー画面とプレビュー画面を生成してから以下のメソッドを実行するだけです。
// overlayViewControlelr:プレイヤー画面 // previewingViewController;プレビュー画面 // isExpanded:プレイヤーを表示するかどうか setOverlayViewController(overlayViewController, previewingViewController: previewingViewController, isExpanded: false)実装
OverlayTabBarControllerというクラスをUITabBarControllerのサブクラスとして作成しました。
タブバーの上(横)にプレイヤー画面を持てるTabBarControllerです。タブバー
UITabBarControllerのタブバーは位置を調整するのが難しかったので、デフォルトのタブバーは見えないようにして同じ見た目のタブバーを追加しました。
// 実際にユーザーに見せるタブバーのセットアップ private func setupFlexibleTabBar() { tabBar.isHidden = true // デフォルトのタブバーは隠しておく if let flexibleTabbar = flexibleTabBar { flexibleTabbar.removeFromSuperview() } flexibleTabBar = UITabBar() flexibleTabBar.barStyle = tabBar.barStyle flexibleTabBar.translatesAutoresizingMaskIntoConstraints = false view.addSubview(flexibleTabBar) /* iPhoneの場合はデフォルトのタブバーと同じようにレイアウトする iPadの場合はプレイヤー画面を右側に設置するので、その分幅を小さくする */ updateFlexibleTabBarConstraints() //デフォルトのタブバーのタブをコピーする updateFlexibleTabBarItems() /* プレビュー画面用のタブバーを設置する iPhoneの場合はプレビュー画面がない場合は非表示にする iPadの場合は常にタブバーの右側に表示する */ setupPreviewingTabBar() }このようにデフォルトのタブバーを非表示にし、新たにタブバーを追加することでAutoLayoutで自由に位置を調整することができました。
プレビュー画面(previewingViewController)
プレビュー画面はプレイヤーを表示していない時にタブバーの上、または横に表示されている画面です。
画像の「Tap Here」と書かれているところです。タブバーと見た目を統一するため、背景にUITabBarを配置しています。(プレビュー画面の背景を透明にする必要があります。)
プレイヤー画面(overlayViewController)
プレビュー画面をタップするとプレイヤー画面が表示されます。
setOverlayViewControllerメソッドでプレイヤー画面とプレビュー画面をセットします。
画面の横幅などの条件からどのように表示するかを決定します。private func setOverlayViewController(_ overlayViewController: UIViewController, previewingViewController: UIViewController, isExpanded: Bool, animated: Bool = true, viewHeight: CGFloat?) { self.previewingViewController = previewingViewController self.overlayViewController = overlayViewController addChild(previewingViewController) if isHorizontalSizeClassRegular { setOverrideTraitCollection(UITraitCollection(traitsFrom: [UITraitCollection(horizontalSizeClass: .regular), UITraitCollection(verticalSizeClass: .regular)]), forChild: previewingViewController) setOverrideTraitCollection(UITraitCollection(traitsFrom: [UITraitCollection(horizontalSizeClass: .regular), UITraitCollection(verticalSizeClass: .regular)]), forChild: overlayViewController) } else { setOverrideTraitCollection(UITraitCollection(traitsFrom: [UITraitCollection(horizontalSizeClass: .compact), UITraitCollection(verticalSizeClass: .regular)]), forChild: previewingViewController) setOverrideTraitCollection(UITraitCollection(traitsFrom: [UITraitCollection(horizontalSizeClass: .regular), UITraitCollection(verticalSizeClass: .regular)]), forChild: overlayViewController) } isOverlayViewExpanded = isExpanded setupPreviewingTabBar() setupButterflyHandle() previewingTabBar.addSubview(previewingViewController.view) previewingViewController.didMove(toParent: self) overlayViewController.view.isHidden = !isExpanded //presentsOverlayViewAsModalでモーダル表示するかどうかを判断する if presentsOverlayViewAsModal(viewHeight: viewHeight) { // モーダル表示する場合は自動的に角丸がつくので、角丸をつける処理を行わない overlayViewController.view.layer.masksToBounds = false overlayViewController.view.layer.cornerRadius = 0 if isExpanded { presentOverlayViewController(animated: animated, completion: nil) } } else { // モーダル表示でない場合はChildViewControllerとして登録する addChild(overlayViewController) view.addSubview(overlayViewController.view) setupOverlayViewConstraints(viewHeight: viewHeight) overlayViewController.view.layer.masksToBounds = true overlayViewController.view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] overlayViewController.view.layer.cornerRadius = 12 overlayViewController.didMove(toParent: self) } setupGestureRecognizers(view: previewingViewController.view) setupGestureResponder() gestureResponder?.isUserInteractionEnabled = isOverlayViewExpanded view.bringSubviewToFront(flexibleTabBar) isOverlayViewTemporaryRemoved = false }UITabBarControllerにChildViewControllerを追加するとタブバーにそれが表示されることがあります。
プレビュー画面とプレイヤー画面をChildViewControllerとして登録してもタブバーに表示されないようにするため、以下のようにviewControllersをoverrideしています。
やっていることとしては、viewControllersにoverlayViewControllerとpreviewingViewControllerがあれば、除外しているだけです。override open var viewControllers: [UIViewController]? { set { guard let overlayViewController = overlayViewController, let previewingViewController = previewingViewController else { super.viewControllers = newValue return } // プレビュー画面とプレイヤー画面が含まれていたら除外する super.viewControllers = newValue?.filter({ $0 != overlayViewController && $0 != previewingViewController }) } get { guard let overlayViewController = overlayViewController, let previewingViewController = previewingViewController else { return super.viewControllers } // プレビュー画面とプレイヤー画面が含まれていたら除外する return super.viewControllers?.filter({ $0 != overlayViewController && $0 != previewingViewController }) } }あまり綺麗なやり方ではないのかもしれないですが、他に思いつかなかったのです。。
画面サイズ変更時の処理
iPadのSplitViewや画面回転に対応するため、以下のメソッドをオーバーライドしています。
willTransitionとtraitCollectionDidChangeだけではtraitCollectionに変化がないiPadの画面回転に対応できないので、viewWillTransitionもオーバーライドしています。override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) guard !isOverlayViewTemporaryRemoved else { return } // 一旦OverlayViewControllerを画面から取り外す temporaryRemoveOverlayViewController() setupButterflyHandle() setupGestureResponder() // 新しい画面サイズに合わせてOverlayViewControllerを設置する layoutOverlayView(viewHeight: size.height) } override open func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { super.willTransition(to: newCollection, with: coordinator) guard traitCollection.horizontalSizeClass != newCollection.horizontalSizeClass else { return } guard overlayViewController != nil else { return } // 一旦OverlayViewControllerを画面から取り外す temporaryRemoveOverlayViewController() setupButterflyHandle() setupGestureResponder() } override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) guard traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass else { return } // 新しい画面サイズに合わせてOverlayViewControllerを設置する layoutOverlayView() } private func layoutOverlayView(viewHeight: CGFloat? = nil) { if let previewView = previewingViewController?.view { previewView.setNeedsLayout() } setupFlexibleTabBar() if let overlayViewController = overlayViewController, let previewViewController = previewingViewController { setOverlayViewController(overlayViewController, previewingViewController: previewViewController, isExpanded: isOverlayViewExpanded, animated: false, viewHeight: viewHeight) updateView(viewHeight: viewHeight) } view.setNeedsLayout() }最後に
Musicアプリのタブバーではタブバーの幅が小さくなるとiPhoneのタブバーのようにタブアイテムのアイコンの下にタイトルが表示されるようになるのですが、OverlayTabBarControllerではそれが実現できず、常にアイコンの横にタイトルが表示されています。
これを実現する方法をご存知の方がいましたら、是非とも教えていただきたいです!