- 投稿日:2019-10-08T23:11:22+09:00
<iOS> 「iOS10以上」と「iOS9.3.5以下」を互換性を持たせてCoreDataを使う
目的
今回、初めてXcodeを触って、アプリを作ってみた。
ただ、XcodeとSwiftのバージョンが新しいのに、実機で持っていたのが「iOS9.3.5」だった。仮想デバイスですら「iOS10」なのにである。デバッグしてみると、悲しいことにいろんなところで調べたことが古くて使えないよ!と言われ、困り果て、何とかして両方使えるように共存できないか探ってみたら、何と似たようなことで困っていた人が記事にしてくれていたので、何とか助かった。
調べても、問題が解決しない記事が多かったので、今回自分でまとめることにした。備忘録。
何が問題?
「iOS10以降」と「それ以下」の違いが大きく関わっている。
・CoreDataの宣言の仕方が割と違う。
→ そもそも、AppDelegateで宣言されているものが違うので、使えるメソッドも違う。・AppDelegateで宣言されているものが違う
→ 前述したように、宣言されているものが違うので、Contextの宣言方法もまるで違う。※因みに、古いXcodeで作ればいいやんと思ってやってみたけど、macOS Mojaveでは、うまくXcodeの共存(つまり、Xcodeのバージョンが古いのと新しいのを両方とも置くこと)ができない。起動時にエラーが出る。(StackOverFlowでも探したけど、みんな同じことが起こって諦めている感じだった。)
対応方法
ここでやりたいのは「共存」。どっちでも使えるようにしたいと考え、以下のように対応した。
- 古いXcodeで宣言されているコードを、新しい方に移植する。
無ければ入れれば良い話で、そこからメソッドを呼び出せばいい。
古いバージョンのXcodeで宣言されているAppDelegateのメソッドを、新しいXcodeのAppDelegateに記述すれば、古い方からもそのバージョンにあったContextを呼び出せるという考え。
今回作ったアプリ
AppDelegate
AppDelegate.swiftimport UIKit import CoreData @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { /*ここまでは、新しいXCodeでは自動的に定義されている。 ------------------------------------------------------------------- ここから下が、古いXcodeからの移植 */ lazy var applicationDocumentsDirectory: NSURL = { let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return urls[urls.count-1] as NSURL }() lazy var managedObjectModel: NSManagedObjectModel = { // The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model. //forResource: には自分のAppの名前を入れる。 let modelURL = Bundle.main.url(forResource: "MyAppName", withExtension: "momd")! return NSManagedObjectModel(contentsOf: modelURL)! }() lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { // The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail. // Create the coordinator and store let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) //appendingPathComponentの引数には、データベース名.sqlite let url = self.applicationDocumentsDirectory.appendingPathComponent("MyDB.sqlite") var failureReason = "There was an error creating or loading the application's saved data." do { try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) } catch { // Report any error we got. var dict = [String: AnyObject]() dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject? dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject? dict[NSUnderlyingErrorKey] = error as NSError let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict) // Replace this with code to handle the error appropriately. // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)") abort() } return coordinator }() lazy var managedObjectContext: NSManagedObjectContext = { // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail. let coordinator = self.persistentStoreCoordinator var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) managedObjectContext.persistentStoreCoordinator = coordinator return managedObjectContext }() /*ここまでをまずは移植する。 ------------------------------------------------------------------------ */ // MARK: - Core Data Saving support @available(iOS 10.0, *) func saveContext () { let context = persistentContainer.viewContext if context.hasChanges { do { try context.save() } catch { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. let nserror = error as NSError fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } } //iOS9以下のCoreData_Saveメソッド これも移植したもの。 func saveContext9 () { if managedObjectContext.hasChanges { do { try managedObjectContext.save() } catch { // Replace this implementation with code to handle the error appropriately. // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. let nserror = error as NSError NSLog("Unresolved error \(nserror), \(nserror.userInfo)") abort() } } } }Contextの宣言
ViewController.swiftfunc getData(){ //ここでiOSのバージョンによって処理が変わる。 if #available(iOS 10.0, *) { //Contextの宣言(iOS10) let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext do{ sample = try context.fetch(Sample.fetchRequest()) }catch{ print("Error") } }else{ //Contextの宣言(iOS9) let context = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext do{ sample = try context.fetch(Sample.fetchRequest()) }catch{ print("Error") } } }CoreData-Save編-
Add.swiftif #available(iOS 10.0, *) { let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext let defaultAction:UIAlertAction = UIAlertAction(title: "OK!", style: UIAlertAction.Style.default) { (action: UIAlertAction!) -> Void in let sample = Sample(context: context) sample.id = Int32(self.AutoIncrements()) sample.name = self.nameField.text sample.mail = self.mailField.text sample.pass = self.passField.text (UIApplication.shared.delegate as! AppDelegate).saveContext() self.navigationController!.popViewController(animated: true) } alert.addAction(defaultAction) } else { // Fallback on earlier versions let context9:NSManagedObjectContext = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext let defaultAction:UIAlertAction = UIAlertAction(title: "OK!", style: UIAlertAction.Style.default) { (action: UIAlertAction!) -> Void in //ここからが新しいXcodeと違うところ let entity = NSEntityDescription.entity(forEntityName: "Sample", in: context9) let sample = NSManagedObject(entity: entity!, insertInto: context9) as! Sample sample.id = Int32(self.AutoIncrements()) sample.name = self.nameField.text sample.mail = self.mailField.text sample.pass = self.passField.text (UIApplication.shared.delegate as! AppDelegate).saveContext9() self.navigationController!.popViewController(animated: true) } alert.addAction(defaultAction) }終わりに
まだまだ記録できていないところはあるけれど、とりあえずここまで。
暇なときに追記していきます。参考サイト
-- xcode8でCoreData使うときにiOS9以前も対応に含めるときに --
https://qiita.com/fujiwarawataru/items/12903b6e8fe42d6464f2ここでヒントをもらいました。@fujiwarawataruさん。この記事を書いていただき感謝です。助かりました。
- 投稿日:2019-10-08T23:11:22+09:00
<iOS:Swift> 「iOS10以上」と「iOS9.3.5以下」を互換性を持たせてCoreDataを使う
目的
今回、初めてXcodeを触って、アプリを作ってみた。
ただ、XcodeとSwiftのバージョンが新しいのに、実機で持っていたのが「iOS9.3.5」だった。仮想デバイスですら「iOS10」なのにである。デバッグしてみると、悲しいことにいろんなところで調べたことが古くて使えないよ!と言われ、困り果て、何とかして両方使えるように共存できないか探ってみたら、何と似たようなことで困っていた人が記事にしてくれていたので、何とか助かった。
調べても、問題が解決しない記事が多かったので、今回自分でまとめることにした。備忘録。
何が問題?
「iOS10以降」と「それ以下」の違いが大きく関わっている。
・CoreDataの宣言の仕方が割と違う。
→ そもそも、AppDelegateで宣言されているものが違うので、使えるメソッドも違う。・AppDelegateで宣言されているものが違う
→ 前述したように、宣言されているものが違うので、Contextの宣言方法もまるで違う。※因みに、古いXcodeで作ればいいやんと思ってやってみたけど、macOS Mojaveでは、うまくXcodeの共存(つまり、Xcodeのバージョンが古いのと新しいのを両方とも置くこと)ができない。起動時にエラーが出る。(StackOverFlowでも探したけど、みんな同じことが起こって諦めている感じだった。)
対応方法
ここでやりたいのは「共存」。どっちでも使えるようにしたいと考え、以下のように対応した。
- 古いXcodeで宣言されているコードを、新しい方に移植する。
無ければ入れれば良い話で、そこからメソッドを呼び出せばいい。
古いバージョンのXcodeで宣言されているAppDelegateのメソッドを、新しいXcodeのAppDelegateに記述すれば、古い方からもそのバージョンにあったContextを呼び出せるという考え。
今回作ったアプリ
AppDelegate
AppDelegate.swiftimport UIKit import CoreData @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { /*ここまでは、新しいXCodeでは自動的に定義されている。 ------------------------------------------------------------------- ここから下が、古いXcodeからの移植 */ lazy var applicationDocumentsDirectory: NSURL = { let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return urls[urls.count-1] as NSURL }() lazy var managedObjectModel: NSManagedObjectModel = { // The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model. //forResource: には自分のAppの名前を入れる。 let modelURL = Bundle.main.url(forResource: "MyAppName", withExtension: "momd")! return NSManagedObjectModel(contentsOf: modelURL)! }() lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { // The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail. // Create the coordinator and store let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) //appendingPathComponentの引数には、データベース名.sqlite let url = self.applicationDocumentsDirectory.appendingPathComponent("MyDB.sqlite") var failureReason = "There was an error creating or loading the application's saved data." do { try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) } catch { // Report any error we got. var dict = [String: AnyObject]() dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject? dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject? dict[NSUnderlyingErrorKey] = error as NSError let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict) // Replace this with code to handle the error appropriately. // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)") abort() } return coordinator }() lazy var managedObjectContext: NSManagedObjectContext = { // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail. let coordinator = self.persistentStoreCoordinator var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) managedObjectContext.persistentStoreCoordinator = coordinator return managedObjectContext }() /*ここまでをまずは移植する。 ------------------------------------------------------------------------ */ // MARK: - Core Data Saving support @available(iOS 10.0, *) func saveContext () { let context = persistentContainer.viewContext if context.hasChanges { do { try context.save() } catch { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. let nserror = error as NSError fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } } //iOS9以下のCoreData_Saveメソッド これも移植したもの。 func saveContext9 () { if managedObjectContext.hasChanges { do { try managedObjectContext.save() } catch { // Replace this implementation with code to handle the error appropriately. // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. let nserror = error as NSError NSLog("Unresolved error \(nserror), \(nserror.userInfo)") abort() } } } }Contextの宣言
ViewController.swiftfunc getData(){ //ここでiOSのバージョンによって処理が変わる。 if #available(iOS 10.0, *) { //Contextの宣言(iOS10) let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext do{ sample = try context.fetch(Sample.fetchRequest()) }catch{ print("Error") } }else{ //Contextの宣言(iOS9) let context = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext do{ sample = try context.fetch(Sample.fetchRequest()) }catch{ print("Error") } } }CoreData-Save編-
Add.swiftif #available(iOS 10.0, *) { let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext let defaultAction:UIAlertAction = UIAlertAction(title: "OK!", style: UIAlertAction.Style.default) { (action: UIAlertAction!) -> Void in let sample = Sample(context: context) sample.id = Int32(self.AutoIncrements()) sample.name = self.nameField.text sample.mail = self.mailField.text sample.pass = self.passField.text (UIApplication.shared.delegate as! AppDelegate).saveContext() self.navigationController!.popViewController(animated: true) } alert.addAction(defaultAction) } else { // Fallback on earlier versions let context9:NSManagedObjectContext = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext let defaultAction:UIAlertAction = UIAlertAction(title: "OK!", style: UIAlertAction.Style.default) { (action: UIAlertAction!) -> Void in //ここからが新しいXcodeと違うところ let entity = NSEntityDescription.entity(forEntityName: "Sample", in: context9) let sample = NSManagedObject(entity: entity!, insertInto: context9) as! Sample sample.id = Int32(self.AutoIncrements()) sample.name = self.nameField.text sample.mail = self.mailField.text sample.pass = self.passField.text (UIApplication.shared.delegate as! AppDelegate).saveContext9() self.navigationController!.popViewController(animated: true) } alert.addAction(defaultAction) }終わりに
まだまだ記録できていないところはあるけれど、とりあえずここまで。
暇なときに追記していきます。参考サイト
-- xcode8でCoreData使うときにiOS9以前も対応に含めるときに --
https://qiita.com/fujiwarawataru/items/12903b6e8fe42d6464f2ここでヒントをもらいました。@fujiwarawataruさん。この記事を書いていただき感謝です。助かりました。
- 投稿日:2019-10-08T22:08:39+09:00
SwiftUI(View)とUIKit(UIView)と同じ画面にだしてsinkさせてみる
今回試してみたこと
今回はUIKitで作成したViewControllorとSwiftUIで作成したViewを一緒に表示させることに挑戦してみたいと思います。
おそらく新規プロジェクトでなければ、SwiftUIに移行することになったときに必ずやることになると思ったからです。
図にするとこんな感じ
とりあえず共有するデータを定義
final class MyData: ObservableObject { @Published var name: String = "" }そして、SwiftUIでViewを定義
struct TestView: View { @EnvironmentObject var myData: MyData var body: some View { HStack { Text(myData.name) .frame(minWidth: 100) Button(action: { self.myData.name = Int(arc4random_uniform(50000)).description }) { Text("タップして乱数生成") }.frame(minWidth: 100) } } }frameのminWidthやminHeightはよく使うことになりそうです。
これでボタンをタップしたら乱数を生成してTextで表示させるシンプルなViewが出来ました。
こんな感じ
次はUIViewを作る
class MyTestUIView: UIView { /// 共有するデータ var data: MyData /// 今回はコードでviewを生成します。MyDataをこのときに渡してます。 init(frame: CGRect, data: MyData) { self.data = data super.init(frame: frame) // UIKitで生成したViewをわかりやすくするためにyellowにしました backgroundColor = UIColor.yellow // textFieldに値を反映し、修正も行います let textField = UITextField(frame: CGRect(x: 100, y: 100, width: 100, height: 100)) addSubview(textField) textField.borderStyle = .roundedRect // (1) textFieldからのデータInputを処理する NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: nil, queue: OperationQueue.main) { notification in guard let text = (notification.object as? UITextField)?.text else { return } data.name = text } // (2) SwiftUIからInputが発生したときにTextFieldにも反映させる _ = data.$name.receive(on: DispatchQueue.main).sink() { textField.text = $0 } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }確認するところは2点あります
(1) textFieldからのデータInputを処理する
NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: nil, queue: OperationQueue.main) { notification in guard let text = (notification.object as? UITextField)?.text else { return } data.name = text }TextFieldの通知を活用してtextFieldの入力が発生したときのイベントをハンドリングしてみました。
textFieldのtextをdata.nameに代入します。もうこれでSwiftUIで定義したViewに反映されます。
HStack { Text(myData.name) .frame(minWidth: 100)こちらの
myData.name
は@Published
のおかげてデータが変更されたらTextが即時に更新されます(2) SwiftUIからInputが発生したときにTextFieldにも反映させる
_ = data.$name.receive(on: DispatchQueue.main).sink() { textField.text = $0 }
@Published
のnameの変更をmainスレッドでreceiveして、sinkでクロージャーを登録してデータが変わったことを知らせてもらっています。
とりあえずやってみたいことは出来ました。
シンプルにしたかったのでtextFieldの変更が入るたびにデータが更新されtextFieldもまた更新されるといった、無駄なことをやってるソースコードにも見えますがw
例えばメモ帳アプリで
上部はUIKItのカレンダーライブラリーで暦の表示
下部はSwiftUIで選択された日付のデータを表示
presentされた画面でイベントが登録されたら両方に反映とかがあり得るのかな?
他の書き方でも似たようなことは出来ると思うのでなにかあればぜひコメントもお願いします!
- 投稿日:2019-10-08T20:40:28+09:00
iOS13 UISegmentControlのBeforeAfterを見てみる
概要
iOS13からUISegmentedControlの見た目と動きが大きく変更になりました。
これまではButtonが複数あるタブっぽい動きでしたが、UISwitchのような動きに変わりましたね。
iOS13では他の新機能が目立って取り上げられていたので、UIKitの見た目変更があると全く知らなくて
開発中アプリの動作確認をしていたら、激変していてびっくりしました汗
実装する上でも変更点があったので備忘録としてまとめていきます。変更点
iOS7からはUISegmentControlの色の指定は
tintColor
でしていましたが、
iOS13以降はこれが使えなく、代わりにselectedSegmentTintColorが追加されました。
機能的には全く同じ。
そう、プロパティ名が長くなっただけ←どちらもアプリで対応する場合は
available
を使用してバージョン管理が必要です。SampleViewController.swiftimport UIKit class SampleViewController: UIViewController { @IBOutlet weak var segmentControl: UISegmentedControl! override func viewDidLoad() { super.viewDidLoad() // 背景色 segmentControl.backgroundColor = .white // ベースカラー if #available(iOS 13.0, *) { segmentControl.selectedSegmentTintColor = .red } else { segmentControl.tintColor = .red } // 通常時の文字色 segmentControl.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], for: .normal) // 選択時の文字色 segmentControl.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], for: .selected) } }ちなみに、背景色に白色を指定すると影がついたようなグレーになってしまいます。。
これはそういう仕様のようで色々頑張ってみましたが背景を完全な白にできませんでした。。
どうしてもしたい場合は画像を背景にするしかないのかな?汗また、iOS12以前のSegmentControlのように外枠をつけてみました。
SampleViewController.swift// 上記のコードに追記 segmentControl.layer.borderColor = UIColor.red.cgColor segmentControl.layer.borderWidth = 1 segmentControl.layer.cornerRadius = 4ちょっと外枠と内側のタブの間に隙間ができるんですね。なるほど。。
iOS12以前のものはパッと見変化がないですが、cornerRadius
の値によって角丸のところが色が固まっているみたいになりました。。
下手に外枠はつけない方が良さそうですかね汗参考
- 投稿日:2019-10-08T20:19:04+09:00
バリデーションをメソッドチェーンで書く方法
アプリ開発で、ユーザ入力が伴う画面では必ずと言っていいほど入力内容のバリデーションが必要です。ところが、もしかしてそのバリデーションのコード、読みにくすぎ?
例えばこんな要件があるとします:ユーザのメンバー ID を入力する画面があります。この ID は 10 桁以内の数字です。さてこれのバリデーションを書きましょうか:
func isIDValid(_ id: String) -> Bool { guard !id.isEmpty else { return false } guard Int(id) != nil else { return false } guard id.count < 10 else { return false } return true } validate("123") // true validate("abc") // falseうーん読めなくはないですが、
guard
文多いですね。今回のような単純な要件ならまあ読めなくはないですが…
あと書くのだるい…もちろん、どうせ
Bool
返してるだけなので、いっそのことこんな感じで書くのもいいですね:func validate(_ id: String) -> Bool { return !id.isEmpty && Int(id) != nil && id.count <= 10 } validate("123") validate("abc")これならだいぶ書きやすくなります。ただやっぱ補完効きにくいのでまだ改善の余地はあるし、何よりそもそもの話、バリデーション結果を
Bool
で返すのはなんかな…ですよね。と言うわけで、これをこんな風に書きたいのです:
func validate(_ id: String) -> ValidationStatus<InvalidID> { IDValidator.validate(id) { $0 .isNotEmpty() .isInt() .lessThan10Digits() } } validate("123") // .valid validate("abc") // .invalid(.notInt)これではメソッドチェーンが使えるのでだいぶ書きやすいし、何よりなんのバリデーションか一目瞭然ですね!
では、これをどうやって作っているのかと言うと、簡単です。まずバリデーションの型を作ります:
protocol InvalidStatus: Equatable { } enum ValidationStatus<Invalid: InvalidStatus> { case valid case invalid(Invalid) }バリデーション状態は
.valid
と.invalid
のみにし、具体的な.invalid
状態はさらにInvalid
で定義してもらいます。これのメリットは、大きなスコープで見た時は有効と無効だけで分けられるのでいろんな柔軟な対応がしやすい1し、具体的になぜ無効なのかが知りたいときのその無効状態をInvalid: InvalidStatus
から取り出せます。次に、このバリデーションを実際にかける型を作ります。とは言え、どっちかと言うとこれはラッパーオブジェクトに近いですかね:
struct ValidationContainer<Target, Invalid: InvalidStatus> { private let target: Target private let invalid: Invalid? private func finish() -> ValidationStatus<Invalid> { if let invalid = invalid { return .invalid(invalid) } else { return .valid } } static func validate(_ target: Target, with validation: (Self) -> Self) -> ValidationStatus<Invalid> { let container = Self.init(target: target, invalid: nil) let result = validation(container).finish() return result } func guarantee(_ condition: (Target) -> Bool, otherwise invalidStatus: Invalid) -> Self { // If the container already has an invalid status, skip the condition check. guard invalid == nil else { return self } if condition(target) == true { return self } else { return ValidationContainer(target: target, invalid: invalidStatus) } } }このコンテナは何かと言うと、現在のバリデーション対象と無効状態を保存して、そして次のバリデーションに回すためのものです。まずは無効状態なしで作って、そして次々と
guarantee
を回して、もしどこか無効状態が途中であったらそれをそのまま返して、最後まで問題なかったらfinish()
で.valid
を返せばいいです。あれ?
validate
メソッドにfinish
は登場したけどguarantee
登場してなくない?と思うかもしれませんが、guarantee
は実際の利用する側に公開したメソッドで、それはvalidate
のvalidation: (Self) -> Self
と言うクロージャ内で利用してもらう予定だからです。ちなみに
struct
にも関わらずSelf
が書けるのは、Swift 5.1 の新機能です2。さて、ここまでで下準備ができました。ではどう利用すればいいでしょうか?まずは
InvalidStatus
を作ります。今回は ID のバリデーションなので、InvalidID
を作りましょう:enum InvalidID: InvalidStatus { case empty case notInt case tooLong(maxCount: Int) }見ての通り、ID が空か、数字じゃないか、もしくは文字数が多すぎるの 3 パターンの無効判定があります。そしてその次、それぞれの判定を
guarantee
を使って組み込みます:extension ValidationContainer where Target == String, Invalid == InvalidID { func isNotEmpty() -> Self { return guarantee({ !$0.isEmpty }, otherwise: .empty) } func isInt() -> Self { return guarantee({ Int($0) != nil }, otherwise: .notInt) } func lessThan10Digits() -> Self { let maxDigits = 10 return guarantee({ $0.count <= maxDigits }, otherwise: .tooLong(maxCount: maxDigits)) } }これで
文字列は空文字列じゃない
、文字列は数字である
と文字列は最大 10 文字以内
のバリデーションが書けました。最後はtypealias
を定義してこれらを繋げば、読みやすいバリデーションが書けますね!typealias IDValidator = ValidationContainer<String, InvalidID> func validate(_ id: String) -> ValidationStatus<InvalidID> { IDValidator.validate(id) { $0 .isNotEmpty() .isInt() .lessThan10Digits() } } validate("123") // .valid validate("abc") // .invalid(.notInt)めでたしめでたし。
p.s. この書き方なんか見覚えあるぞ?と思ってくれた方もしいらっしゃったら、ありがとうございます!!そうですあの NotAutoLayout でも使ってたクロージャ×メソッドチェーンの組み方です
- 投稿日:2019-10-08T19:42:38+09:00
ピンチアウトでズーム可能なUIImageViewを作成する。
ストーリーボードの準備
ViewControllerのsubViewにScrollViewを配置します。
ImageViewはコードで追加します。
ソースコード
ModalImageViewController.swiftimport UIKit class ModalImageViewController: UIViewController, UIScrollViewDelegate { @IBOutlet weak var scrollView: UIScrollView! var image: UIImage? = nil var imageUrl: String = "" var imageView = UIImageView() override func viewDidLoad() { super.viewDidLoad() // デリゲートを設定 self.scrollView.delegate = self // 最大倍率・最小倍率を設定する self.scrollView.maximumZoomScale = 5.0 self.scrollView.minimumZoomScale = 1.0 // imageViewをscrollViewいっぱいに生成 self.imageView.frame = CGRect(x:0,y:0,width:view.frame.width,height:view.frame.height) scrollView.addSubview(imageView) self.imageView.contentMode = .scaleAspectFit guard let _noImage = UIImage(named:"noimage") else { return } self.loadImage(self.imageUrl, _noImage) } func viewForZooming(in scrollView: UIScrollView) -> UIView? { return self.imageView } func scrollViewDidZoom(_ scrollView: UIScrollView) { // ズーム終了時の処理 } func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { // ズーム開始時の処理 } func zoomForScale(scale:CGFloat, center: CGPoint) -> CGRect{ var zoomRect: CGRect = CGRect() zoomRect.size.height = self.scrollView.frame.size.height / scale zoomRect.size.width = self.scrollView.frame.size.width / scale zoomRect.origin.x = center.x - zoomRect.size.width / 2.0 zoomRect.origin.y = center.y - zoomRect.size.height / 2.0 return zoomRect } /// 画像を非同期で取得して、ImageViewに設定します。 /// - Parameter urlString: url文字列 /// - Parameter defaultImage: 取得失敗したときの画像を入れてください private func loadImage(_ urlString: String, _ defaultImage: UIImage) -> Void { guard let _url = URL(string: urlString) else { self.imageView.isHidden = true return } DispatchQueue.global().async { do { let imageData: Data? = try Data(contentsOf: _url) DispatchQueue.main.async { if let data = imageData, let image = UIImage(data: data) { self.imageView.image = image } else { self.imageView.image = defaultImage } } } catch { DispatchQueue.main.async { self.imageView.image = defaultImage } } } } }実行結果
- 投稿日:2019-10-08T18:31:42+09:00
UITextView UILabel で指定していないTimes New Romenという Font が使用される
環境
Xcode 11.1
Swift 4.2
iOS13事象
最新の環境で、ビルドするとUITextView UILabelのテキストが、明朝体風の Times New Romen へ変わってしまう問題が発生しました。
Debug view Hierarchy上では、"Times New Romen"というフォントが設定されてしまう。
コード上で設定しても、上書きできず。
暫定対応
NSAttributedStringを使う場合のみ指定したフォントが読み込まれるようです。
あくまで暫定対応なので、アップデートで治る可能性もあるため、急いで直したい場合は、以下で対応できました。let stringAttributes: [NSAttributedStringKey : Any] = [.font : UIFont.systemFont(ofSize: 14.0)] textView.attributedText = NSAttributedString(string: "text", attributes: stringAttributes)追記:2019/10/08 18:30 現在、iOS 13.1.2, Xcode 11.1でも直らない
参考
- 投稿日:2019-10-08T18:31:42+09:00
UITextView UILabel で Times New Romen が使用される
環境
Xcode 11.1
Swift 4.2
iOS13事象
最新の環境で、ビルドするとUITextView UILabelのテキストが、明朝体風の Times New Romen へ変わってしまう問題が発生しました。
Debug View Hierarchy上で確認してみると Times New Romen というフォントが設定されてしまう。
暫定対応
NSAttributedStringを使う方法とFontを再設定する方法があります。
あくまで暫定対応なので、アップデートで治る可能性もあるため、急いで直したい場合は、以下で対応できました。let stringAttributes: [NSAttributedStringKey : Any] = [.font : UIFont.systemFont(ofSize: 14.0)] textView.attributedText = NSAttributedString(string: "text", attributes: stringAttributes)or
// customクラスを作成する class SystemFontTextView: UITextView { override var text: String! { didSet { font = UIFont.systemFont(ofSize: font?.pointSize ?? 14) } } }追記:2019/10/08 18:30 現在、iOS 13.1.2, Xcode 11.1でも直らない
参考
- 投稿日:2019-10-08T16:16:10+09:00
【初心者】UIの設計にはAdobeのXDが超絶便利だった件
はじめに
こんにちは、僕です。今回はアプリ設計の経験がなかった僕がとりあえずUI作成にと使用したツール「AdobeXD」が超絶便利だったので初心者の方用に参考になればと思い書きました。
使用環境
Adobe XD 22.7.12.3 (スターター)
良かった点
無料
有料版もありますが、まずはスターターで十分です。
このソフトをタダで使えるなんて良い時代に産まれました。操作性の良いUI
何と言っても一番のメリットはここでしょう。おしゃれなアプリ作成には持ってこいです。本当に使いやすい。
他にいくつかUIツールを試しましたが、UIツールなのにそもそもソフト自体のUIが悪いソフトが多すぎます。
それにくらべAdobeXDは白を基調にしたInstagramやAirbnbのなどに似たシンプルなデザインで初心者にも使いやすい用に作られています。
「アプリを設計したいが、専門的な知識はない」
「でもストアでよく見るあんなダサいデザインにしたくはない」
と思う方にとっては最高のツールではないかと思います。さらに、これはアプリ開発の話ですがデバイスプレビュー機能という実際にスマホで動かしているような画面で作品を確認する機能もついています。簡単にですがこのボタンでどこに飛ぶといった設定もできるのでおすすめです。さらに、USB接続で専用アプリを入れたiPhoneとリアルタイムで同期し、プレビューができるので、絶対に使いましょう。
テンプレートが多い
AdobeXDでは世界中のクリエイターが作ったテンプレートがネットに転がっています。
Adobe XDで簡単にプロトタイプを作れる!おすすめの無料テンプレート15選
とても質のいいテンプレートがたくさんあります。
基本的なフレームワークができていればある程度感覚でなんとかなるものです。
特に僕と同じような学生で他の学生とデザインで差を人にとってはこのちょっとした努力で評価がかなり変わってくるでしょう!
これは僕がテンプレートを参考に一時間程度で作ったフリマアプリのプロフィール画面ですが、デザイン知識のない学生でもここまでできるものです。
なお、デザイン画面はSVGでエクスポートすることでXcode等でもそのまま使えるので安心ですね。最後に
僕と同じように「デザインの知識がないがアプリのデザインをしたい」という方には本当にぴったりのツールだと思います。
ダウンロードは下記からできます。ぜひお試しあれ。
- 投稿日:2019-10-08T14:08:54+09:00
This application’s application-identifier entitlement does not match that of the installed application. These values must match for an upgrade to be allowed
- 投稿日:2019-10-08T11:06:38+09:00
[swift]トルツメ機能を実装する
はじめに
Web画面とかでよくあるトルツメ機能が欲しかったのですがUIKITなどにはもちろんなく、一から手作りしたので共有します。
画面のイメージ
コード
ViewController.swiftimport UIKit class ViewController: UIViewController { // トルツメ設定 let horizontalStackView:UIStackView = UIStackView() let verticalStackView:UIStackView = UIStackView() override func viewDidLoad() { super.viewDidLoad() // 画面の大きさを取得 let screenWidth = Int( UIScreen.main.bounds.size.width); let screenHeight = Int( UIScreen.main.bounds.size.height); print("screenWidth=\(screenWidth)") print("screenHeight=\(screenHeight)") // 横用StackView view.addSubview(horizontalStackView) horizontalStackView.translatesAutoresizingMaskIntoConstraints = false // オートレイアウトオフ horizontalStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true // 上のラベル 横並びにするためにダミーのViewも作成する let label = UIView() label.backgroundColor = UIColor.clear label.translatesAutoresizingMaskIntoConstraints = false label.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth/4)).isActive = true label.heightAnchor.constraint(equalToConstant: 40).isActive = true let label2 = UIView() label2.backgroundColor = UIColor.clear label2.translatesAutoresizingMaskIntoConstraints = false label2.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth/4)).isActive = true label2.heightAnchor.constraint(equalToConstant: 40).isActive = true let label3 = UIView() label3.backgroundColor = UIColor.clear label3.translatesAutoresizingMaskIntoConstraints = false label3.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth/4)).isActive = true label3.heightAnchor.constraint(equalToConstant: 40).isActive = true let label4 = UIButton() label4.backgroundColor = UIColor.gray label4.translatesAutoresizingMaskIntoConstraints = false label4.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth/4)).isActive = true label4.heightAnchor.constraint(equalToConstant: 40).isActive = true // 画像 let image:UIImage = UIImage(named:"menu")! let imageView = UIImageView(image:image) label4.addSubview(imageView) // addしてからじゃないと制約追加できないので注意 imageView.translatesAutoresizingMaskIntoConstraints = false imageView.centerXAnchor.constraint(equalTo: label4.centerXAnchor).isActive = true imageView.centerYAnchor.constraint(equalTo: label4.centerYAnchor).isActive = true imageView.contentMode = UIView.ContentMode.scaleAspectFit // 縦用StackView view.addSubview(verticalStackView) verticalStackView.translatesAutoresizingMaskIntoConstraints = false verticalStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true verticalStackView.axis = NSLayoutConstraint.Axis.vertical // 縦並び verticalStackView.alpha = 0.8 // メインView let mainView = UIView() mainView.backgroundColor = UIColor.gray mainView.translatesAutoresizingMaskIntoConstraints = false mainView.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth)).isActive = true mainView.heightAnchor.constraint(equalToConstant: CGFloat(screenHeight/10)).isActive = true // 項目AのLabel let itemName = UILabel() itemName.textColor = .white itemName.text = "項目A: テスト" mainView.addSubview(itemName) itemName.translatesAutoresizingMaskIntoConstraints = false itemName.centerXAnchor.constraint(equalTo: mainView.centerXAnchor, constant: 15).isActive = true itemName.centerYAnchor.constraint(equalTo: mainView.centerYAnchor, constant: CGFloat(screenHeight/60)).isActive = true itemName.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth)).isActive = true itemName.font = UIFont.boldSystemFont(ofSize: 21) // 項目BのLabel let torishoName = UILabel() torishoName.textColor = .white torishoName.text = "項目B: test" mainView.addSubview(torishoName) torishoName.translatesAutoresizingMaskIntoConstraints = false torishoName.centerXAnchor.constraint(equalTo: mainView.centerXAnchor, constant: 15).isActive = true torishoName.centerYAnchor.constraint(equalTo: mainView.centerYAnchor, constant: -CGFloat(screenHeight/60)).isActive = true torishoName.widthAnchor.constraint(equalToConstant: CGFloat(screenWidth)).isActive = true torishoName.font = UIFont.boldSystemFont(ofSize: 21) horizontalStackView.addArrangedSubview(label) horizontalStackView.addArrangedSubview(label2) horizontalStackView.addArrangedSubview(label3) horizontalStackView.addArrangedSubview(label4) verticalStackView.addArrangedSubview(horizontalStackView) verticalStackView.addArrangedSubview(mainView) // 表示時はhiddenとする verticalStackView.arrangedSubviews[1].isHidden = true // ボタンを押した時 label4.addTarget(self, action: #selector(pushButton), for: .touchUpInside) } // ボタン押したとき @objc func pushButton(sender: UIButton){ //ここにViewを隠したり表したりする処理を入れる verticalStackView.arrangedSubviews[1].isHidden.toggle() verticalStackView.layoutIfNeeded() } }StackViewを組み合わせて実現しました。
もっといい方法あるよ!などあったら教えてください。
- 投稿日:2019-10-08T09:11:58+09:00
【Swift】 「タブを閉じる」ボタンの追加(自作タブブラウザアプリ開発)
このエントリ(【Swift】 タブブラウザライクに、任意のボタンを押した際に対応するViewを最前面に表示する)の続きとして、各タブに「タブを閉じるボタン」をつけました。
開発環境
端末:MacBook Pro/MacOS 10.14.5(Mojave)
Xcode:10.2.1
Swift:5やったこと(ポイント)
①各タブに「タブを閉じる」ボタンを追加
実装
画面イメージ
各タブの左に、赤い「×」でボタンを付けています。
3つのタブが表示された状態
「tabNumber2」以外を閉じた状態 見た目では分からないですが、「×」ボタンを押すと、対応するタブボタンとそれに紐づいているWKWebViewも消えています。
あと、消したタブの右側にタブが存在する場合、ちゃんと左に詰められるようにしています。ソースコード
冒頭にリンクを貼ったエントリから、修正した点は大きく2つです。
①タブに「閉じる(×)」ボタンを追加表示させる
②「閉じる」ボタン押下時の処理を追加するそれ以外の部分を割愛すると、以下のようなコードで実現しています。
ViewController.swiftimport UIKit import WebKit class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { ... // 前回のエントリに貼ったコードを参照。 @objc func closeTab(sender: UIButton) { // webViewを消す let viewTag = sender.tag + 9000 let targetView = view.viewWithTag(viewTag) as! WKWebView targetView.removeFromSuperview() // タブボタンを消す let btnTabTag = sender.tag - 1000 let targetBtn = view.viewWithTag(btnTabTag) as! UIButton targetBtn.removeFromSuperview() // タブボタンにひっついていた「閉じる」ボタン自体も消す let btnCloseTabTag = sender.tag let targetBtnCloseTab = view.viewWithTag(btnCloseTabTag) as! UIButton targetBtnCloseTab.removeFromSuperview() // UIScrollViewの中身を残りのタブで配置しなおす originX = 0 tabScrollView.frame = CGRect.init(x: 0, y: 44, width: viewWidth, height: 50) tabScrollView.contentSize = CGSize.init(width: 0, height: 50) for i in 1...tagNumber { if (view.viewWithTag(i) != nil) { let targetButton = view.viewWithTag(i) as! UIButton let targetButtonWidth:CGFloat = 200 let targetButtonHeight:CGFloat = tabScrollView.frame.height // タブボタンの見た目設定 targetButton.tag = i targetButton.layer.cornerRadius = 3.0 targetButton.layer.borderWidth = 1.5 targetButton.layer.borderColor = UIColor.init(red: 87/255, green: 144/255, blue: 40/255, alpha: 1).cgColor targetButton.backgroundColor = .white targetButton.setTitleColor(UIColor.init(red: 118/255, green: 197/255, blue: 57/255, alpha: 1), for: .normal) targetButton.titleLabel?.font = UIFont.systemFont(ofSize: 15) targetButton.addTarget(self, action: #selector(showView(sender: )), for: .touchUpInside) targetButton.frame = CGRect(x: originX, y: 0, width: targetButtonWidth, height: targetButtonHeight) // タブを閉じるボタン let closeTagNumber = i + 1000 let targetCloseButton = view.viewWithTag(closeTagNumber) as! UIButton let targetCloseButtonWidth:CGFloat = 30 let targetCloseButtonHeight:CGFloat = 30 targetCloseButton.tag = closeTagNumber targetCloseButton.addTarget(self, action: #selector(closeTab(sender:)), for: .touchUpInside) targetCloseButton.frame = CGRect.init(x: originX + 160, y: 10, width: targetCloseButtonWidth, height: targetCloseButtonHeight) // スクロールバーの中のタブボタンの表示位置をずらす制御 originX += targetButtonWidth tabScrollView.addSubview(targetButton) tabScrollView.addSubview(targetCloseButton) tabScrollView.contentSize = CGSize(width: originX, height: targetButtonHeight) print("originX = " + originX.description) view.addSubview(tabScrollView) } } } func addTab(url: URL) { ... // 前回のエントリに貼ったコードを参照。 // タブを閉じるボタン let btnCloseTab = UIButton() let btnCloseTabWidth:CGFloat = 30 let btnCloseTabHeight:CGFloat = 30 btnCloseTab.tag = tagNumber + 1000 btnCloseTab.titleLabel?.textAlignment = .center btnCloseTab.setTitle("×", for: .normal) btnCloseTab.setTitleColor(.red, for: .normal) btnCloseTab.addTarget(self, action: #selector(closeTab(sender:)), for: .touchUpInside) btnCloseTab.frame = CGRect.init(x: originX + 160, y: 10, width: btnCloseTabWidth, height: btnCloseTabHeight) // スクロールバーの中のタブボタンの表示位置をずらす制御 originX += tabButtonWidth // タブボタンと、閉じるボタンをUIScrollViewに追加 tabScrollView.addSubview(tabButton) tabScrollView.addSubview(btnCloseTab) tabScrollView.contentSize = CGSize(width: originX, height: tabButtonHeight) view.addSubview(tabScrollView) view.addSubview(webView) ... }一応「閉じる」機能を実装できました。
他の良いやり方が思いつかないですが、なんかビミョーな感じはしてます。補足
細かいトコですが、前回のエントリではタブボタンの文字をタブ中央に配置していたんですが、「閉じる」ボタンの追加にあたり、左寄せにしました。
しかし、そのやり方が
UIButton.titleLabel.textAlignment = .left
ではなく、
UIButton.contentHorizontalAlignment = .left
だったので、一応補足まで。let tabButton = UIButton() let tabButtonWidth:CGFloat = 200 let tabButtonHeight:CGFloat = tabScrollView.frame.height // タブボタンの見た目設定 tabButton.tag = tagNumber // この設定はあまり意味がなかった。 // tabButton.titleLabel?.textAlignment = .center tabButton.contentHorizontalAlignment = .left感想など
「タブを閉じる」はできたので、次は「今開いてるタブはどれか」を示すために色を変えていたのを、タブを閉じた時にどう制御するかを考えていきます。
あと、実際動かしたいWebサービスでは、target="hoge"
のように開くタブが指定されているリンクがあるので、それをきちんと制御してあげるにはどうしたらいいかが課題かなと思っています。引き続き頑張ります。
以上です。
- 投稿日:2019-10-08T04:14:25+09:00
xcodeでいきなりCould not raunch MyApp と更にprocess launch failed unspecifiedとエラー実機でのデバッグ不可!!
xcodeでいきなりCould not raunch MyApp と更にprocess launch failed unspecifiedとエラー実機でのデバッグ不可となった。
更にキーチェーンアクセスにて証明書が信頼されてないとのメッセージ。
https://www.apple.com/certificateauthority/
上記サイトよりAppleWWDRCA.cerファイルをダウンロードクリックでキーチェーンアクセスへ追加登録
- 投稿日:2019-10-08T02:13:35+09:00
[SwiftUI]リスト表示のセルにボタンが配置できない問題とその解決策
いわゆる、UITableViewCellの上にUIButtonを置くような事がしたい場合にSwiftUIでどのように記述すれば良いでしょうか。
struct ContentView: View { var body: some View { List { Group { HStack { Text("Tap ME!! →") Button(action: { print("tap!!") }, label: { Text("Button") }) } } } } }このように、Listの中でButtonを書けば良さそうです。では実行してみましょう。
レイアウトは良さそうですが、ボタンが黒色ですね。
もうお気づきかと思いますが、Listの中で宣言するButtonは選択可能なCellを表示することを表現します。
同様に、複数のButtonが存在してもセルをタップした際に全てのボタンが反応するセルになってしまいます。セル内にボタン(っぽいもの)を配置したい場合は、
struct ContentView: View { var body: some View { List { Group { HStack { Text("Tap ME!! →") Text("Button").onTapGesture { print("tap!!") } } } } } }このように、onTapGestureなどで強制的に反応するようにしてあげれば良いです。