- 投稿日:2019-10-02T22:54:59+09:00
Mapbox for iOSでサードパーティ製のスタイルを設定する
はじめに
地図系のフレームワークで有名で、最近ではゼンリンとの提携がニュースになったりしたMapboxはiOS向けにもフレームワークを提供しています。
Mapbox for iOSの導入
ただ地図を表示するだけなら、チュートリアルのとおりとてもシンプルに書けます。
import Mapbox class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let url = URL(string: "mapbox://styles/mapbox/streets-v11") //Mapbox公式スタイル let mapView = MGLMapView(frame: view.bounds, styleURL: url) mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] mapView.setCenter(CLLocationCoordinate2D(latitude: 59.31, longitude: 18.06), zoomLevel: 9, animated: false) view.addSubview(mapView) } }しかし上記コードのコメントにも書いたとおり、基本的には公式スタイルを設定するように作られています。
スタイルは、.json形式のデータにも関わらず、です。
公式スタイルを参照するのが最もラクですが、そのためにはAPIトークンが必要で、アクセス数に応じて料金が発生します。
ならば、OpenStreetMapなどオープンデータを活用し、公式スタイルを使わなければ良いのではないか?
という事で色々試しました。サードパーティ製のスタイルを適用する
Mapboxのスタイルは、MGLMapViewの初期化時にstyleURLという引数で設定されます。
つまりURLを渡さなければなりませんが、どこかのサーバーにデータを置いておく、というのはナンセンスです。
という訳で、①好きなデータ等を設定したスタイルを.json形式で作成する、②①のローカルアドレスを取得しMGLMapViewに渡す、という方針になりました。①:スタイルを.json形式で作成する
Swiftでの.jsonの取り扱い等は本記事では割愛します。
スタイルの.jsonファイル、言い換えると辞書形式のデータ構造は以下のとおりです。private var style:[String:Any] = [ "version":8, "sources":[], "layers":[], ]versionは8で固定です。sourcesはレイヤーデータの形式等の設定、layersはsourcesのデータをどのように表示するか(色など)の設定となっています。
つまりsourcesとlayersに適切な初期データを与えて、.jsonデータを作成すれば良いわけです。mutating func setBasemap(name:String, tileUrlStr:String, attributionUrl:String="", tileSize:Int=256) { let sources = [ name:[ "type":"raster", "tiles":[tileUrlStr], "attribution":"<a href='" + attributionUrl + "'>" + name + "</a>", "tileSize":tileSize ] ] let layers = [ [ "id":name, "type":"raster", "source":name, "minzoom":0, "maxzoom":18 ] ] self.style["sources"] = sources self.style["layers"] = layers }上記のコードは、とあるラスターレイヤーをスタイルに設定するサンプルです。
レイヤーのnameとタイルのtileUrlStrを与えてやればスタイルに追記します。
.jsonファイルの出力については、長くなるのでサンプルだけ貼ります。func writeJson(outputDir:String, filename:String) -> URL? { let nsHomeDir = NSHomeDirectory() let outputPath = nsHomeDir + outputDir + "/" + filename + ".json" do { let jsonData = try JSONSerialization.data(withJSONObject: self.style, options: .prettyPrinted) try jsonData.write(to: URL(fileURLWithPath: outputPath)) return URL(fileURLWithPath: outputPath) } catch { print("error") return nil } }②:①のローカルアドレスを取得しMGLMapViewに渡す
①のローカルアドレスは、writeJson()の返り値です。つまり以下のとおり書けます。
let tmpStyleUrl = msManager.writeJson(outputDir: "/tmp", filename: "style") mapView = MGLMapView(frame: rect, styleURL: tmpStyleUrl!)ここで、msManagerとはスタイル全般を取り扱うクラスです。
アプリを起動する度に、writeJson()でiOS内の/tmpにstyle.jsonとして出力しています。
/tmpは、アプリを終了した後のデータの保存は保証されない、一次保存用フォルダです。
今回のような用途にもってこいですね。さて、これで純正スタイルを設定する必要がなくなりました。
API tokenを削除してみましたが、問題なく動作します。API tokenはMapboxスタイルにアクセスするためのものでした。
- 投稿日:2019-10-02T21:46:17+09:00
Xcode11で作成したプロジェクトでは`[UIWindow new]`で生成したWindowが画面に表示されない問題の対処法
Xcode11とUISceneとUIWindowにまつわるトラブルシュートに追われている。
個別のケースに分解してお届け。現象
- Xcode11で作成したプロジェクトをiOS13で実行しているとき、UIWindowが表示されない
- rootViewController持たせて、
viewDidLoad
にbreak pointを貼ったりすると、動作はしていることが確認できる。原因
- Xcode11で作る新規プロジェクトは、Scene Based Lifecycleが有効になっている。
- Scene Based Lifecycleが有効になっていると、
[UIWindow new]
,[[UIWindow alloc] initWithFrame:]
で生成したwindowは View Hierarchyに載らなくなる
- UIWindowを載せるべきWindowSceneが不明なため
対応
ひとつは、Xcode11で作成したプロジェクトからSceneに関する機能をオミットする方法。
もうひとつは正攻法。(本記事)
[[UIWindow alloc] initWithWindowScene:]
で windowを生成する。そのためには、なんらかの手段によって
UIWindowScene
を拾い上げる必要がある。ボタンを押した時に表示したいとか、特定のViewと同じUIWindowSceneを選択する場合
ViewController.m@interface ViewController () @property (nonatomic) UIWindow *window; @end @implementation ViewController - (IBAction)openWindow:(UIButton *)sender { UIWindowScene *scene = sender.window.windowScene; UIWindow *window = [[UIWindow alloc] initWithWindowScene: scene]; // 以下は確認用の見た目調整と表示 UIViewController *viewController = [UIViewController new]; viewController.view.backgroundColor = UIColer.greenColer; window.frame = CGRectMake(20, 20, 50, 50); window.rootViewContoller = viewController; self.window = window; [self.window makeKeyAndVisible]; } @endViewController.swiftclass ViewController: UIViewController { var window: UIWindow? @IBAction func openWindow(_ sender: UIButton) { guard let scene = sender.window?.windowScene { return } let window = UIWindow(windowScene: scene) // 以下は確認用の見た目調整と表示 let viewController = UIViewController() viewController.view.backgroundColer = .green window.frame = CGRect(x:20, y:20, width:50, height:50) window.rootViewContoller = viewController self.window = window self.window?.makeKeyAndVisible(); } }Sceneは一つだけのアプリである場合など、UIApplicationからUIWindowSceneを引っ張りたい場合
ViewController.m@interface ViewController () @property (nonatomic) UIWindow *window; @end @implementation ViewController - (IBAction)openWindow:(UIButton *)sender { UIWindowScene *scene = (UIWindowScene*)[[[[UIApplication sharedApplication] connectedScenes] allObjects] first]; UIWindow *window = [[UIWindow alloc] initWithWindowScene: scene]; // 以下は確認用の見た目調整と表示 UIViewController *viewController = [UIViewController new]; viewController.view.backgroundColor = UIColer.greenColer; window.frame = CGRectMake(20, 20, 50, 50); window.rootViewContoller = viewController; self.window = window; [self.window makeKeyAndVisible]; } @endViewController.swiftclass ViewController: UIViewController { var window: UIWindow? @IBAction func openWindow(_ sender: UIButton) { guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene { return } let window = UIWindow(windowScene: scene) // 以下は確認用の見た目調整と表示 let viewController = UIViewController() viewController.view.backgroundColer = .green window.frame = CGRect(x:20, y:20, width:50, height:50) window.rootViewContoller = viewController self.window = window self.window?.makeKeyAndVisible(); } }参考URL
- 投稿日:2019-10-02T21:46:17+09:00
Xcode11で作成したプロジェクトでは`[UIWindow new]`で生成したWindowが画面に表示されない
Xcode11とUISceneとUIWindowにまつわるトラブルシュートに追われている。
個別のケースに分解してお届け。現象
- Xcode11で作成したプロジェクトをiOS13で実行しているとき、UIWindowが表示されない
- rootViewController持たせて、
viewDidLoad
にbreak pointを貼ったりすると、動作はしていることが確認できる。原因
- Xcode11で作る新規プロジェクトは、Scene Based Lifecycleが有効になっている。
- Scene Based Lifecycleが有効になっていると、
[UIWindow new]
,[[UIWindow alloc] initWithFrame:]
で生成したwindowは View Hierarchyに載らなくなる
- UIWindowを載せるべきWindowSceneが不明なため
対応
ひとつは、Xcode11で作成したプロジェクトからSceneに関する機能をオミットする方法。
もうひとつは正攻法。(本記事)
[[UIWindow alloc] initWithWindowScene:]
で windowを生成する。そのためには、なんらかの手段によって
UIWindowScene
を拾い上げる必要がある。ボタンを押した時に表示したいとか、特定のViewと同じUIWindowSceneを選択する場合
ViewController.m@interface ViewController () @property (nonatomic) UIWindow *window; @end @implementation ViewController - (IBAction)openWindow:(UIButton *)sender { UIWindowScene *scene = sender.window.windowScene; UIWindow *window = [[UIWindow alloc] initWithWindowScene: scene]; // 以下は確認用の見た目調整と表示 UIViewController *viewController = [UIViewController new]; viewController.view.backgroundColor = UIColer.greenColer; window.frame = CGRectMake(20, 20, 50, 50); window.rootViewContoller = viewController; self.window = window; [self.window makeKeyAndVisible]; } @endViewController.swiftclass ViewController: UIViewController { var window: UIWindow? @IBAction func openWindow(_ sender: UIButton) { guard let scene = sender.window?.windowScene { return } let window = UIWindow(windowScene: scene) // 以下は確認用の見た目調整と表示 let viewController = UIViewController() viewController.view.backgroundColer = .green window.frame = CGRect(x:20, y:20, width:50, height:50) window.rootViewContoller = viewController self.window = window self.window?.makeKeyAndVisible(); } }Sceneは一つだけのアプリである場合など、UIApplicationからUIWindowSceneを引っ張りたい場合
ViewController.m@interface ViewController () @property (nonatomic) UIWindow *window; @end @implementation ViewController - (IBAction)openWindow:(UIButton *)sender { UIWindowScene *scene = (UIWindowScene*)[[[[UIApplication sharedApplication] connectedScenes] allObjects] first]; UIWindow *window = [[UIWindow alloc] initWithWindowScene: scene]; // 以下は確認用の見た目調整と表示 UIViewController *viewController = [UIViewController new]; viewController.view.backgroundColor = UIColer.greenColer; window.frame = CGRectMake(20, 20, 50, 50); window.rootViewContoller = viewController; self.window = window; [self.window makeKeyAndVisible]; } @endViewController.swiftclass ViewController: UIViewController { var window: UIWindow? @IBAction func openWindow(_ sender: UIButton) { guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene { return } let window = UIWindow(windowScene: scene) // 以下は確認用の見た目調整と表示 let viewController = UIViewController() viewController.view.backgroundColer = .green window.frame = CGRect(x:20, y:20, width:50, height:50) window.rootViewContoller = viewController self.window = window self.window?.makeKeyAndVisible(); } }参考URL
- 投稿日:2019-10-02T12:56:02+09:00
【Swift】基本事項の意味確認
・クラス メソッドやプロパティの集合体で一つの塊
・メソッド 関数のこと
・プロパティ 定数や変数のこと
・インスタンス 何らかの値が入った変数や定数のこと
・インスタンス化 実際に使えるようにすること例えるとクラスは洋式の部屋、メソッドやプロパティは家具(概念として)、インスタンスは実際に部屋に置いてある家具、インスタンス化は実際に家具を使用すること。
- 投稿日:2019-10-02T12:14:25+09:00
MVVM用のXcodeのテンプレート作ってみた
最近RxSwiftを使ったMVVMを勉強してるのですが毎回ViewController作って、ViewModel作って、連結部分書いてってやるの流石に面倒になってきたのでXcodeのテンプレート作ることにしました。
既存のテンプテートはここにあるみたいです。
/Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File\ Templates/
1から作るのはめんどくさそうなので一番シンプルそうなSwiftファイルのテンプレートをコピーしていじっていこうと思います。
Custom
というディレクトリ名で作ってみます。sudo mkdir /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File\ Templates/Custom/そして既存のSwiftファイルのテンプレートをコピーしてくる、ついで
MVVM.xctemplate
にリネームsudo cp -r /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File\ Templates/Source/Swift\ File.xctemplate /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File\ Templates/Custom/MVVM.xctemplate確認してみます。
open /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File\ Templates/
___FILEBASENAME___.swift
を編集していきます。
一回の操作でViewControllerとViewModelを作りたかったので自分はこんな感じにしました。
___VARIABLE_NAME___ViewController.swift//___FILEHEADER___ import UIKit import RxSwift import RxCocoa class ___VARIABLE_NAME___ViewController: UIViewController { private var disposeBag = DisposeBag() private let viewModel = ___VARIABLE_NAME___ViewModel() override func viewDidLoad() { super.viewDidLoad() configure() } private func configure() { //outputs //inputs viewModel.inputs.configure() } }___VARIABLE_NAME___ViewModel.swift//___FILEHEADER___ import Foundation import RxSwift import RxCocoa protocol ___VARIABLE_NAME___ViewModelInputs { func configure() } protocol ___VARIABLE_NAME___ViewModelOutputs { } protocol ___VARIABLE_NAME___ViewModelType { var inputs: ___VARIABLE_NAME___ViewModelInputs { get } var outputs: ___VARIABLE_NAME___ViewModelOutputs { get } } final class ___VARIABLE_NAME___ViewModel: ___VARIABLE_NAME___ViewModelType, ___VARIABLE_NAME___ViewModelInputs, ___VARIABLE_NAME___ViewModelOutputs { //Properties private var disposeBag = DisposeBag() var inputs: ___VARIABLE_NAME___ViewModelInputs { return self } var outputs: ___VARIABLE_NAME___ViewModelOutputs { return self } //Functions func configure() { } }TemplateInfo.plist<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Kind</key> <string>Xcode.IDEFoundation.TextSubstitutionFileTemplateKind</string> <key>Description</key> <string>An empty Swift file.</string> <key>Summary</key> <string>An empty Swift file</string> <key>SortOrder</key> <string>30</string> <key>AllowedTypes</key> <array> <string>public.swift-source</string> </array> <key>DefaultCompletionName</key> <string>File</string> <key>MainTemplateFile</key> <string>___FILEBASENAME___.swift</string> <!-- 追加ここから --> <key>Options</key> <array> <dict> <key>Identifier</key> <string>NAME</string> <key>Required</key> <true/> <key>Name</key> <string>名称を入力してください。 Name:</string> <key>Description</key> <string>「Hoge」と入力するとHogeViewController,HogeViewModelが生成されます。</string> <key>Type</key> <string>text</string> <key>Default</key> <string>Hoge</string> </dict> </array> <!-- 追加ここまで --> </dict> </plist>変数の利用方法などこちらのサイト様を参考にさせていただきました。
設定は以上になります。確認してみましょう。
New File...
からの
↓↓↓ここで決めたファイル名は反映されないので適当にEnter押しちゃってください
良い感じに出来てますね!
気がかりはファイル名を決める画面が全く機能してないところですね、できればスキップしたい、、、
なにか回避策があればコメントいただけると嬉しいです。今回のコードは↓↓↓にアップしてます。
https://github.com/akasasan454/Xcode_template
- 投稿日:2019-10-02T10:04:20+09:00
【Swift】WKWebViewでピンチによるズームを無効にする
import UIKit import WebKit class WebViewController: UIViewController { @IBOutlet weak var webView: WKWebView! override func viewDidLoad() { super.viewDidLoad() guard let url: URL = Bundle.main.url(forResource: "index", withExtension: "html") else { return } webView.scrollView.delegate = self webView.loadFileURL(url, allowingReadAccessTo: url) // webView.scrollView.pinchGestureRecognizer?.isEnabled = false // ↑↑↑この時点で書いてもpinchGestureRecognizerがnilなので意味ない } } extension WebViewController: UIScrollViewDelegate { func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { scrollView.pinchGestureRecognizer?.isEnabled = false // ↑↑↑delegateの中で書いてやると良い } }
- 投稿日:2019-10-02T09:17:22+09:00
Macアプリ初心者:Realm で削除されたコレクションに対して自動でUIに反映
前回作成したプロジェクトではRealmで更新された情報を自動的にアップデートするための処理を実装してみましたが、今回はにRealmを使ってみましたが、値が更新された時にUIも連動して更新されるようにしてみました。
Watchリスト風にしてみました。次回は追加機能を作ります。完成イメージ
環境
- macOS Mojave:10.14.6
- Xcode:11.0
データの更新イメージ
Watchリスト風にするために、前回作成した時からデータ構造を変更しています。
株価情報をマスターデータにしてWatchリストへの表示/非表示をフラグで管理します。データクラスの変更
データ構造を変更
Watchリストの表示/非表示をフラグとして、
isWatchList
を追加しています。StockInfo.swiftclass StockInfo: Object { @objc dynamic var name = "" @objc dynamic var price = -1 @objc dynamic var status = StockStatus.flat.rawValue @objc dynamic var isWatchList = true @objc dynamic var createdAt = Date() @objc dynamic var updatedAt = Date() }データ構造変化に伴い処理の追加と変更
isWatchList
の更新用の関数を追加StockInfo.swiftextension StockInfo { /// watch list の表示/非表示を切り替える func update(isWatchList: Bool) { let realm = try! Realm() try! realm.write { self.isWatchList = isWatchList self.updatedAt = Date() } }検索用の関数にWatchリストの表示/非表示をフラグを指定できるように修正
StockInfo.swift/// 企業名で部分一致検索(空の場合は全件取得) class func objects(isWatchList: Bool? = true, searchValue: String = "") -> Results<StockInfo> { var result = try! Realm().objects(StockInfo.self) let searchValue = searchValue.trimmingCharacters(in: .whitespaces) if let isWatchList = isWatchList { result = result.filter("isWatchList == %@", isWatchList) } if searchValue != "" { result = result.filter("name CONTAINS %@", searchValue) } return result.sorted(byKeyPath: "createdAt", ascending: true) } }Watchリストからの削除用UIを作成
カスタムセルのイメージ
以前に作成したセルの右側に新しくカスタムViewとNSButtonを配置してます。
×ボタンクリック時に表示フラグをオフ
以前のプロジェクトからリファクタリングして、株価情報Objectをカスタムセルに渡して株価情報の描画と更新処理を行うようにしてます。
データオブジェクトをUIクラスで保持するのはどうなんだって話もありますが、わかりやすいのでこんな感じで作ってしまってます。CustomNSTableCellView.swiftclass CustomNSTableCellView: NSTableCellView { // MARK: - public variables var stockInfo: StockInfo? { didSet { guard let stockInfo = self.stockInfo else { return } self.companyName.stringValue = stockInfo.name self.stockImage.image = stockInfo.stockStatus.image self.companyName.textColor = stockInfo.stockStatus.color self.stockImage.contentTintColor = stockInfo.stockStatus.color let formatter = NumberFormatter() formatter.numberStyle = .decimal let commaString = formatter.string(from: NSNumber(value: stockInfo.price))! self.stockPrice.stringValue = commaString } } // MARK: - IBAction @IBAction func actionDelete(_ sender: NSButton) { self.stockInfo?.update(isWatchList: false) } }カスタムセルのI/Fを変更したのでそれに合わせて、ViewController 側も変更
ViewController.swiftextension ViewController: NSTableViewDataSource, NSTableViewDelegate { // MARK: - NSTableViewDelegate func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let result = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "MyView"), owner: self) as! CustomNSTableCellView // Set the stringValue of the cell's text field to the nameArray value at row result.stockInfo = self.stockValues[row] // Return the result return result } }ソースコード
展開して確認
ViewController.swiftimport Cocoa import RealmSwift class ViewController: NSViewController { // MARK: - IBOutlet @IBOutlet private weak var tableView: NSTableView! { didSet { self.tableView.dataSource = self self.tableView.delegate = self self.tableView.headerView = nil self.tableView.rowHeight = 88 } } @IBOutlet private weak var countTextField: NSTextField! // MARK: - private variables private var stockValues = StockInfo.objects() private var timer: Timer? = nil private var notificationToken: NotificationToken? = nil // MARK: - override variables override var representedObject: Any? { didSet { // Update the view, if already loaded. } } // MARK: - override func override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // デバッグ用にデータ作成 StockInfo.debugInitData() // 株価情報をViewに反映 self.updateStockValuesCount() // Resultsの通知を監視します notificationToken = self.stockValues.observe { [weak self] (changes: RealmCollectionChange) in guard let tableView = self?.tableView else { return } switch changes { case .initial: // Results are now populated and can be accessed without blocking the UI tableView.reloadData() case .update(let collectionType, let deletions, let insertions, let modifications): // Resultsに変更があったので、UITableViewに変更を適用します print(collectionType) tableView.beginUpdates() tableView.insertRows(at: IndexSet(insertions), withAnimation: .effectFade) tableView.removeRows(at: IndexSet(deletions), withAnimation: .effectFade) tableView.reloadData(forRowIndexes: IndexSet(modifications), columnIndexes: IndexSet(integer: 0)) tableView.endUpdates() case .error(let error): // バックグラウンドのワーカースレッドがRealmファイルを開く際にエラーが起きました fatalError("\(error)") } // 件数を更新 self?.updateStockValuesCount() } self.timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true, block: { (timer) in StockInfo.debugRandomUpdate(stockValues: self.stockValues) }) } deinit { self.notificationToken?.invalidate() self.timer?.invalidate() } // MARK: - private func private func updateStockValuesCount() { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal let commaCount = numberFormatter.string(from: NSNumber(value: self.stockValues.count))! let dateFormatter = DateFormatter() dateFormatter.dateFormat = "HH:mm:ss" let lastUpdatedString = dateFormatter.string(from: Date()) self.countTextField.stringValue = "\(commaCount)件 (last updated:\(lastUpdatedString))" } // MARK: - actions @IBAction func actionSearchField(_ sender: NSSearchField) { self.stockValues = StockInfo.objects(searchValue: sender.stringValue) self.tableView.reloadData() self.updateStockValuesCount() } } extension ViewController: NSTableViewDataSource, NSTableViewDelegate { // MARK: - NSTableViewDataSource func numberOfRows(in tableView: NSTableView) -> Int { return self.stockValues.count } // MARK: - NSTableViewDelegate func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let result = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "MyView"), owner: self) as! CustomNSTableCellView // Set the stringValue of the cell's text field to the nameArray value at row result.stockInfo = self.stockValues[row] // Return the result return result } }CustomNSTableCellView.swiftimport Cocoa class CustomNSTableCellView: NSTableCellView { // MARK: - IBOutlet @IBOutlet private weak var stockImage: NSImageView! @IBOutlet private weak var companyName: NSTextField! @IBOutlet private weak var stockPrice: NSTextField! // MARK: - public variables var stockInfo: StockInfo? { didSet { guard let stockInfo = self.stockInfo else { return } self.companyName.stringValue = stockInfo.name self.stockImage.image = stockInfo.stockStatus.image self.companyName.textColor = stockInfo.stockStatus.color self.stockImage.contentTintColor = stockInfo.stockStatus.color let formatter = NumberFormatter() formatter.numberStyle = .decimal let commaString = formatter.string(from: NSNumber(value: stockInfo.price))! self.stockPrice.stringValue = commaString } } // MARK: - IBAction @IBAction func actionDelete(_ sender: NSButton) { self.stockInfo?.update(isWatchList: false) } }StockInfo.swiftimport Cocoa import RealmSwift class StockInfo: Object { @objc dynamic var name = "" @objc dynamic var price = -1 @objc dynamic var status = StockStatus.flat.rawValue @objc dynamic var isWatchList = true @objc dynamic var createdAt = Date() @objc dynamic var updatedAt = Date() } extension StockInfo { /// 状態をEnum形式で返却 var stockStatus: StockStatus { get { return StockStatus(rawValue: self.status) ?? StockStatus.flat } set { self.status = newValue.rawValue } } /// watch list の表示/非表示を切り替える func update(isWatchList: Bool) { let realm = try! Realm() try! realm.write { self.isWatchList = isWatchList self.updatedAt = Date() } } /// StockInfo オブジェクトを作成 class func create(realm: Realm, name: String, price: Int, status: StockStatus) -> StockInfo { let stockInfo = realm.create(StockInfo.self) stockInfo.name = name stockInfo.price = price stockInfo.stockStatus = status return stockInfo } /// 企業名で部分一致検索(空の場合は全件取得) class func objects(isWatchList: Bool? = true, searchValue: String = "") -> Results<StockInfo> { var result = try! Realm().objects(StockInfo.self) let searchValue = searchValue.trimmingCharacters(in: .whitespaces) if let isWatchList = isWatchList { result = result.filter("isWatchList == %@", isWatchList) } if searchValue != "" { result = result.filter("name CONTAINS %@", searchValue) } return result.sorted(byKeyPath: "createdAt", ascending: true) } } // MARK: - debug extension StockInfo { /// データ初期化 class func debugInitData() { let realm = try! Realm() try! realm.write { // データクリア realm.deleteAll() // サンプルデータ作成 let _ = StockInfo.create(realm: realm, name: "○○株式会社", price: 1000, status: .up) let _ = StockInfo.create(realm: realm, name: "○×水産", price: 12345, status: .down) let _ = StockInfo.create(realm: realm, name: "株式会社□○", price: 345, status: .flat) let _ = StockInfo.create(realm: realm, name: "×△ホールディングス", price: 321, status: .up) let _ = StockInfo.create(realm: realm, name: "ABC BANK", price: 20, status: .down) let _ = StockInfo.create(realm: realm, name: "▼○重工", price: 98000, status: .up) // サンプル用に1,000 件データを作成 // for i in 1...1_000 { // let status = StockStatus.init(rawValue: Int.random(in: 0..<3))! // let price = Int.random(in: -1..<10_000_000) // let _ = StockInfo.create(realm: realm, name: "○△×株式会社 \(i)", price: price, status: status) // } } } /// データ更新 class func debugRandomUpdate(stockValues: Results<StockInfo>) { let realm = try! Realm() try! realm.write { for stockValue in stockValues { let status = StockStatus(rawValue: Int.random(in: 0..<3))! stockValue.stockStatus = status var value = Int(Double(stockValue.price) * 0.05) if value == 0 { value = 1 } switch status { case .down: stockValue.price += -Int.random(in: 0..<value) if stockValue.price < 0 { stockValue.price = 1 } stockValue.updatedAt = Date() case .up: stockValue.price += Int.random(in: 0..<value) stockValue.updatedAt = Date() case .flat: break } } } } }
- 投稿日:2019-10-02T08:20:09+09:00
CombineのPublisherをテストするためのexpect/expectErrorメソッドを作成した
おそらく現在はユニットテストでCombineのPublisherの値を検査するためにはXCTestExpectationを駆使したりしないといけません(簡便な方法があったら教えて欲しいです。)。さらに私はSwiftCheckを利用しているので検査結果をBool値で取得したいという要求があります。
ですのでテストターゲットでexpectとexpectErrorという検査メソッドをPublisherに追加しました。
import XCTest import Combine public extension Publisher where Output: Equatable { @discardableResult func expect(_ expectedValue: Output, takesNewest: Bool = false, timeout: TimeInterval = 2.0, fulfillmentCount: Int = 1, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) -> Bool { var result = false var actualValues = [Output]() var actualFulfillmentCount = 0 var cancellables = Set<AnyCancellable>() let exp = XCTestExpectation() exp.expectedFulfillmentCount = fulfillmentCount exp.assertForOverFulfill = true let waiter = XCTWaiter() self.receive(on: RunLoop.main).sink(receiveCompletion: { _ in }, receiveValue: { if takesNewest || !result { result = $0 == expectedValue } actualValues.append($0) actualFulfillmentCount += 1 exp.fulfill() }).store(in: &cancellables) _ = waiter.wait(for: [exp], timeout: timeout) XCTAssertLessThanOrEqual(fulfillmentCount, actualFulfillmentCount, "\(file) - \(function):\(line): Expectation is short of fulfillment. '\(fulfillmentCount)' expected, but '\(actualFulfillmentCount)'.") XCTAssertTrue(result, "\(file) - \(function):\(line): Expected output is '\(String(describing: expectedValue))', but actual stream is '\(String(describing: actualValues))'") return result && fulfillmentCount <= actualFulfillmentCount } } public extension Publisher where Failure: Equatable { @discardableResult func expectError(_ expectedError: Failure, timeout: TimeInterval = 2.0, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) -> Bool { var actualError: Failure? var cancellables = Set<AnyCancellable>() let exp = XCTestExpectation() let waiter = XCTWaiter() self.receive(on: RunLoop.main).sink(receiveCompletion: { completion in switch completion { case .failure(let receivedError): actualError = receivedError default: () } exp.fulfill() }, receiveValue: { _ in }).store(in: &cancellables) _ = waiter.wait(for: [exp], timeout: timeout) XCTAssertEqual(expectedError, actualError, "\(file) - \(function):\(line): Expected error is '\(String(describing: expectedError))', but actual error is '\(String(describing: actualError))'") return expectedError == actualError } }expectする値が取得できれば成功、できなければ失敗、そして結果のBool値を返すようになっています。
timeoutは値を取得する制限時間です。
expectのtakesNewestは、trueであればストリームの最新の値まで全て検査し、過去に該当する値があっても結果を上書きします。fulfillmentCountは何回値を取得するかを設定します。テストで利用するには以下のように記述します。
class MyTests: XCTestCase { class P: Publisher { typealias Output = Int typealias Failure = Never func receive<S>(subscriber: S) where S : Subscriber, P.Failure == S.Failure, P.Output == S.Input { DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { subscriber.receive(1) subscriber.receive(2) subscriber.receive(completion: .finished) } let subscription = Subscriptions.empty subscriber.receive(subscription: subscription) } } class Q: Publisher { typealias Output = Int typealias Failure = MyError func receive<S>(subscriber: S) where S : Subscriber, Q.Failure == S.Failure, Q.Output == S.Input { DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { subscriber.receive(completion: .failure(.error1)) } let subscription = Subscriptions.empty subscriber.receive(subscription: subscription) } } enum MyError: Error, Equatable { case error1 case error2 } func testExpect() { P().expect(1) P().buffer(size: 2, prefetch: .keepFull, whenFull: .dropOldest).collect().expect([1, 2]) P().expect(1, takesNewest: true, fulfillmentCount: 2) // failed P().expect(3) // failed } func testExpectError() { Q().expectError(.error1) } }私のプロジェクトでは一応動作しているようです。
バグやより良い実装があれば教えてください。
- 投稿日:2019-10-02T06:24:30+09:00
UIKitのカスタムビュー側の値をSwiftUIのView側で取得する方法
はじめに
SwiftUIだけでは用意されていないコンポーネントがあったりでUIKit使いたいなーという状況はよくあるかと思います。
そんな時は、SwiftUIで頑張らないで、UIKitで作ってしまえ...と思い、カスタムビューを作成したのですが、
カスタムビュー側で保持している値
をSwiftUIのView側で使いたい
という状況に遭遇したので、
今回はその方法を備忘録兼ねて残します。また、今回は、SwiftUIの方にないSearchBarをUIKitで作り、
UISearchBar
のテキストフィールドに入力された値をSwiftUI側で取得するというサンプルで試してみます。環境
Xcode11 GM SEED
SwiftUI
Mac OS 10.14.6(18G103)まずXibを使ったカスタムビューを作成しておく
以下のようにカスタムビューを
UIView
で定義しておきます。
今まで通りAutoLayoutはしっかり設定させます。アウトレットでついているのは、
UISearchBar
とカスタムビュー自体のContentViewだけ。
そしてXib側ではLabelを追加しています。
また、見やすいようにUIKitで作成したカスタムビューは青色にしておきます。import UIKit // Define UIKitSearchView in the usual way to create custom views. class UIKitSearchView: UIView { @IBOutlet weak var contentView: UIView! @IBOutlet weak var searchBar: UISearchBar! override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) commonInit() } private func commonInit() { Bundle.main.loadNibNamed("UIKitSearchView", owner: self, options: nil) contentView.frame = bounds addSubview(contentView) searchBar.placeholder = "UIKitSearchView's searchBar" searchBar.backgroundImage = UIImage() } }これでカスタムビューの準備は終わりました。
UIViewRepresentableを準拠したSwiftUISearchViewを定義する
SwiftUIで宣言されたViewの中でUIViewを使う場合は、
UIViewRepresentable
プロトコルを使用したViewでラップすることが必要です。この
SwiftUISearchView
がSwiftUIで使う為のラップビューとなります。import SwiftUI struct SwiftUISearchView: UIViewRepresentable { func makeUIView(context: Context) -> UIKitSearchView { print("\(#function)") return UIKitSearchView() } func updateUIView(_ uiView: UIKitSearchView, context: Context) { print("\(#function)") } }シンプルにUIViewを表示させるだけであれば、この
SwiftUISearchView
を宣言すれば終わりですが、このままではUISearchBar
に入力された文字列を取得することができません。UIViewRepresentableの定義を見てみる
UIKitSearchView側でクロージャを保持するプロパティを定義したり、強引にやればなんとかなりそうですが、スマートじゃないってことで
UIViewRepresentable
のプロトコルの定義を調べてみます。public protocol UIViewRepresentable : View where Self.Body == Never { /// The type of `UIView` to be presented. associatedtype UIViewType : UIView /// Creates a `UIView` instance to be presented. func makeUIView(context: Self.Context) -> Self.UIViewType /// Updates the presented `UIView` (and coordinator) to the latest /// configuration. func updateUIView(_ uiView: Self.UIViewType, context: Self.Context) /// Cleans up the presented `UIView` (and coordinator) in /// anticipation of their removal. static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator) /// A type to coordinate with the `UIView`. associatedtype Coordinator = Void /// Creates a `Coordinator` instance to coordinate with the /// `UIView`. /// /// `Coordinator` can be accessed via `Context`. func makeCoordinator() -> Self.Coordinator typealias Context = UIViewRepresentableContext<Self> } @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) @available(OSX, unavailable) extension UIViewRepresentable where Self.Coordinator == Void { /// Creates a `Coordinator` instance to coordinate with the /// `UIView`. /// /// `Coordinator` can be accessed via `Context`. public func makeCoordinator() -> Self.Coordinator } @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *) @available(OSX, unavailable) extension UIViewRepresentable { /// Cleans up the presented `UIView` (and coordinator) in /// anticipation of their removal. public static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator) /// Declares the content and behavior of this view. public var body: Never { get } }ふむふむ。
なんだかUIViewRepresentableを継承すると、以下の実装が必要となるようです。
makeUIView(context:)
updateUIView(_:context:)
makeCoordinator()
dismantleUIView(_ uiView:coordinator:)
ただ、
makeCoordinator()
、dismantleUIView(_ uiView:coordinator:)
はextensionでデフォルト実装がされているので、定義不要みたいですね。なんかmakeCoordinator()が使えそう
dismantleUIView
は調べたところ、表示されるUIKitビュー(およびコーディネーター)をクリーンアップするのに使うらしい。(よくわかりませんでした。)
どなたか教えていただけると嬉しいです。ということで
makeCoordinator
がなんか使えそう。ググってみると
なんでも、このCoordinator
を使用するとデリゲートやデータソース、ユーザーの操作に対するイベントハンドリングのようなUIKitなどで実装していた一般的な処理をSwiftUIでも実装できるらしい。extension UIViewRepresentable where Self.Coordinator == Void { /// Creates a `Coordinator` instance to coordinate with the /// `UIView`. /// /// `Coordinator` can be accessed via `Context`. public func makeCoordinator() -> Self.Coordinator }
UIViewRepresentable
を継承したSelfにあるCoordinator
がなければ、デフォルト実装をするようになっているみたいなので、
つまりは、さきほど作ったSwiftUISearchView
の中にCoordinator
を作ればいいっぽい。
そして、このCoordinator
にUISearchBarDelegate
をつけちゃいます。
Coordinator
を作ったらmakeCoordinator
も追加して、さらにdelegate
も指定させます。struct SwiftUISearchView: UIViewRepresentable { func makeCoordinator() -> SwiftUISearchView.Coordinator { return Coordinator() } func makeUIView(context: Context) -> UIKitSearchView { print("\(#function)") let view = UIKitSearchView() view.searchBar.delegate = context.coordinator return view } func updateUIView(_ uiView: UIKitSearchView, context: Context) { print("\(#function)") } } extension SwiftUISearchView { class Coordinator: NSObject, UISearchBarDelegate { func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { print("\(#function)") } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { print("\(#function)") } } }おー、これでCoodinatorで通知を受け取れるようになりました。
UISearchBarのテキストに入力した値を取得する
@EnvironmentObject
を使えば簡単ですが、それだと設計上良くなさそう。なので、今回は、
@State
+@Binding
で実現させてみます。おさらい
@State
とは?SwiftUIだとViewはstructなので、値の更新ができないけれども
@Stateを宣言すると、値を更新することができるようになるバインド用の修飾子。
@Binding
とは?親Viewの@State attributeがついた変数などの値の更新通知を受け取れるようにするバインド修飾子のようです。(なんか使えそう...)
僕の中でこいつはまだふわっとしています。アドバイスお待ちしておりますバインドさせるイメージ
1.親のContentViewでtextを保持
2.子のSwiftUISearchViewに親のContentViewで定義したtextを渡して同期させる
3.UISearchBarの変更通知を受けるのはCoordinatorなので、SwiftUISearchViewからCoordinatorへもtextを渡して同期させるこんな感じで行けるんじゃないかと思っています。
それでは、残りをやっていきます。親ビューを作ってtextをバインドさせる
まず親ViewであるContentView作ります。
そこで@StateでUISearchBarからの入力値を保持しておく受け皿を用意しておきます。struct ContentView: View { // 親ビューで値が知りたいので、@Stateでtextを定義しておく @State private var text: String = DEFAULT_TEXT var body: some View { VStack(alignment: .center) { // バインドさせたいのでSwiftUISearchViewの初期化処理で突っ込む SwiftUISearchView(text: $text) .frame(height: 100.0) Spacer() VStack { Text("SwiftUI View") .font(.title) .padding(.bottom, 20) Text(text) } .multilineTextAlignment(.center) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(.blue).opacity(0.5)) .padding() Spacer() } } }SwiftUISearchViewとCoordinatorにもtextを保持するようにプロパティと処理を追加
親ビューに続いて、SwiftUISearchView、Coordinatorにもtextをバインドさせる為に、@Bindingでプロパティを追加し、さらに初期化やデリゲート通知を受けた時の処理を追加しておきます。
struct SwiftUISearchView: UIViewRepresentable { @Binding var text: String func makeCoordinator() -> SwiftUISearchView.Coordinator { return Coordinator(text: $text) } func makeUIView(context: Context) -> UIKitSearchView { print("\(#function)") let view = UIKitSearchView() view.searchBar.delegate = context.coordinator return view } func updateUIView(_ uiView: UIKitSearchView, context: Context) { print("\(#function)") } } extension SwiftUISearchView { class Coordinator: NSObject, UISearchBarDelegate { @Binding var text: String init(text: Binding<String>) { _text = text } func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { print("\(#function)") text = searchText.isEmpty ? DEFAULT_TEXT: searchText } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { print("\(#function)") text = DEFAULT_TEXT searchBar.text = nil } } }ここまでで一旦完成しました。
あとは実行して確認します。結果
実行してみると無事値がSwiftUI側の親ビュー側に同期されていました。
イメージ通りです。
これは@Binding
便利です。github
https://github.com/kazy-dev/UISearchbar-from-SwiftUI-Sample
まとめ
今回は、
UIKitのカスタムビュー側の値
をSwiftUIのView側で取得する
という方法をやってみましたが、なんとかイメージ通りに機能したので、すっきりしました。ただ、これが正しいのかが正直良くわかっていませんので
アドバイスやご指摘などあれば、ぜひぜひコメントお願いいたします。SwiftUI関連は、これから色々最適解が出てくると思いますので、それまではトライアンドエラーでやってみたいと思います。
それでは、最後までお読みいただきありがとうございました。
- 投稿日:2019-10-02T04:17:34+09:00
【Swift】 ボタン押下時に画面にViewを追加する
はじめに
会社で提供してるWebサービス専用のタブブラウザを作れないか、というコトで、実現性評価を何段階かに分けてやってるうちの1つ。
ボタン押下時にUIViewを画面に追加する。開発環境
端末:MacBook Pro/MacOS 10.14.5(Mojave)
Xcode:10.2.1
Swift:5やったこと
ボタン押下時に、画面のランダムな場所に10px*10pxのUIViewを表示。
→ボタンを押下するたびにどんどんUIViewを追加していく。実装
画面イメージ
起動時
ボタン連打 ※色と場所がランダムに設定されたUIViewがボタンを押すたびに画面に配置されていく。
ソースサンプル
ViewController.swiftimport UIKit class ViewController: UIViewController { @IBOutlet weak var btnCreateView: UIButton! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. btnCreateView.center = view.center } @IBAction func createView(_ sender: Any) { let testView: UIView = UIView() let viewWidth = UIScreen.main.bounds.width let viewHeight = UIScreen.main.bounds.height let randX = CGFloat(Int.random(in: 0 ... Int(viewWidth))) let randY = CGFloat(Int.random(in: 0 ... Int(viewHeight))) let randRed = CGFloat(Int.random(in: 0 ... 255)) let randGreen = CGFloat(Int.random(in: 0 ... 255)) let randBlue = CGFloat(Int.random(in: 0 ... 255)) testView.backgroundColor = .init(red: randRed/255, green: randGreen/255, blue: randBlue/255, alpha: 1) testView.frame = CGRect.init(x: randX, y: randY, width: 10, height: 10) view.addSubview(testView) } }感想等
実現性評価というか、まぁこうなるだろうな〜ってイメージを順当に形にした感じです。
- 投稿日:2019-10-02T00:46:40+09:00
SwiftUIでList内のViewは複数の遷移先を持つことができない
iOSアプリを開発する上で、UIKitのUITableViewCellでタップしたUIViewによって遷移先を変えるというのは、よくあると思います。
同じことをSwiftUIのList
とNavigationView
で実現しようとしましたが、うまくいかなかったのですが、その際に試したことを備忘録として残しておきます。
環境はXcode11.0 (11A420a)、iOS13.0です。
NavigationLink
を2つ指定した場合import SwiftUI struct ContentView: View { var body: some View { NavigationView { List { Row(city: "Twentynine Palms", state: "California") Row(city: "Port Alsworth", state: "Alaska") Row(city: "Skagway", state: "Alaska") } } } } struct Row: View { var city: String var state: String var body: some View { VStack(alignment: .leading, spacing: 4) { NavigationLink(city, destination: Text(city)) NavigationLink(state, destination: Text(state)) } } }Pushの遷移を行う場合は
NavigationLink
使用するので、素直に実装すると、このようになるかと思います。
これをビルドすると以下のような挙動になりました。1回のタップで遷移を2回おこなっていて、意図した挙動になっていません。(また表示自体も
disclosureIndicator
が2つ表示されてしまっています)
Button
を2つ配置してNavigationLink.init(destination:isActive:label:)
を使用した場合
NavigationLink
のインターフェイスを調べているとNavigationLink.init(destination:isActive:label:)
というメソッドが定義されています。
ドキュメントには定義以上の情報がないですが、利用できそうです。import SwiftUI struct ContentView: View { var body: some View { NavigationView { List { Row(city: "Twentynine Palms", state: "California") Row(city: "Port Alsworth", state: "Alaska") Row(city: "Skagway", state: "Alaska") } } } } struct Row: View { var city: String var state: String @State var buttonDidTap = (false, "") var body: some View { VStack(alignment: .leading, spacing: 4) { Button(city) { self.buttonDidTap = (true, self.city) } Button(state) { self.buttonDidTap = (true, self.state) } NavigationLink(destination: Text(buttonDidTap.1), isActive: $buttonDidTap.0) { EmptyView() } } } }実装は上記のように各ボタンのアクションを
NavigationLink
にバインドしています。一見上手く遷移できているようですが、どのボタンをタップしても2番目にあるボタンの遷移先になってしまっています。
各ボタンのアクションにブレイクポイントを貼って、もう少し挙動を確認してみます。ボタンを1つだけタップしているはずですが、各ボタンのアクションが実行されてしまっています。
そのため最後のボタンイベントが優先されてしまい、このような挙動になってしまっているようです。最後に
上記のように、素直な方法では実現できませんでした。
そのため、Listを用いた一覧画面は遷移を1つに絞り、詳細画面で複数の遷移先を持つデザインに変更して、解決することにしました。もし他に良い方法を知っている方がいれば教えてほしいです。(UIKitを使えば色々な解決方法はありそうですよね)
参考資料
https://developer.apple.com/documentation/swiftui/navigationlink/3364630-init
- 投稿日:2019-10-02T00:01:20+09:00
【iOS】AESで暗号化・復号する方法(CryptoSwift)
背景
APIと連携するようなサーバー通信時には、平文ではなく暗号化したい。
そこで、CryptoSwift
を用いて簡単に暗号化・復号できるらしいので試してみました。iOSアプリでAESでの暗号化のためCryptoSwiftを使用した
↓↓↓↓↓↓↓↓
自分の記事を作成中にCryptoSwift
の良記事を見つけてしまったので、メモレベルで記載します。笑AESとは
暗号化のやり方の一つ
2018年現在のアメリカで標準として採用されているやり方
共通鍵暗号方式
鍵長が128ビット、192ビット、256ビットから選ぶことができる
WPA2で標準で採用されているやり方
WPAで使われる場合もある
引用:iOSアプリでAESでの暗号化のためCryptoSwiftを使用したバージョン
- Xcode 10.2.1
- Swift 5.0.1
- CryptoSwift 1.0.0
CryptoSwiftライブラリを導入する
CocoaPods
を用いてCryptoSwiftを導入します。Podfile
は以下になります。Podfiletarget 'Qiita_AES' do use_frameworks! # CryptoSwiftライブラリを追加する pod 'CryptoSwift' endCocoaPodsの導入方法がわからない場合は、こちらの記事を参考にしてください。
【Swift】CocoaPods導入手順
暗号化・復号用のEncryptionAESクラスを実装
EncryptionAES.swiftimport Foundation import CryptoSwift class EncryptionAES { // 暗号化処理 // key:変換キー // iv:初期化ベクトル(時間などを使用してランダム生成するとよりセキュアになる) // text:文字列 func encrypt(key: String, iv:String, text:String) -> String { do { // 暗号化処理 // AES インスタンス化 let aes = try AES(key: key, iv: iv) let encrypt = try aes.encrypt(Array(text.utf8)) // Data 型変換 let data = Data( encrypt ) // base64 変換 let base64Data = data.base64EncodedData() // UTF-8変換 nil 不可 guard let base64String = String(data: base64Data as Data, encoding: String.Encoding.utf8) else { return "" } // base64文字列 return base64String } catch { // エラー処理 return "" } } // 複合処理 // key:変換キー // iv:?(時間などを使用してランダム生成するとよりセキュアになる) // base64:文字列 func decrypt(key: String, iv:String, base64:String) -> String { do { // AES インスタンス化 let aes = try AES(key: key, iv:iv) // base64 から Data型へ let byteData = base64.data(using: String.Encoding.utf8)! as Data // base64 デーコード guard let data = Data(base64Encoded: byteData) else { return "" } // UInt8 配列の作成 let aBuffer = Array<UInt8>(data) // AES 複合 let decrypted = try aes.decrypt(aBuffer) // UTF-8変換 guard let text = String(data: Data(decrypted), encoding: .utf8) else { return "" } return text } catch { // エラー処理 return "" } } }呼び出し方法
let key = "abcdefghijklmnop" // 128bit(16文字)のキーを入れる let iv = "1234567890123456" // データをシフト演算するキー128bit(16文字) let json = "{\"id\":2,\"name\":\"ほげほげ君\",\"hobby\":\"ボルダリング\"}" // 暗号化・復号するjson // EncryptionAESのインスタンス化 let aes = EncryptionAES() // jsonを暗号化 let jsonEncrypted = try! aes.encrypt(key: key, iv: iv, text: json) print(jsonEncrypted) // -> PlOEf5oeBt2GsyWDHUk7OtOUV6nRPpYQvqE919jkrrpWaAvQc+IhkrO3gPmiNQbkQOLLXMlnQh5U2wbwEPYksg== // jsonを復号 let jsonDecrypted = aes.decrypt(key: key, iv: iv, base64: jsonEncrypted) print(jsonDecrypted) // -> {"id":2,"name":"ほげほげ君","hobby":"ボルダリング"}まとめ
暗号化・復号と聞くと実装が大変なイメージでしたが、
CryptoSwift
なら簡単にできるのでおすすめです。^^以上になります。
もし不明点や間違い等があればコメントくださいm(_ _)m