- 投稿日:2019-10-21T21:55:10+09:00
Swift勉強の始まり
- 投稿日:2019-10-21T21:40:18+09:00
【Swift】オプショナル型の使い方
オプショナル型とは?
オプショナル型はSwiftの特徴の一つであり、値がnilの可能性がある時に用いられる型です。
このオプショナル型はその値がnilである可能性を示してくれるため非常に便利な型なのですが、扱い方を間違えるとコンパイルエラーになったりバグの要因になります。今回はそんなオプショナル型の使い方について考えていきます。
Optional < Wrapped >型を用いる時
まず下記のコードを見てください。このコードはRPGに出てくる武器の構造体です。定数として名前(name)、攻撃力(offensivePower)、追加効果(additionalEffect)が定義されています。
ここで注目すべき点はadditionalEffectのかたがString?となっている点です。この?がオプショナル型を表していて、この定数がnilの可能性があることを示しています。
このように値が定かではない値にはオプショナル型を定義していきましょう。struct Weapon { let name: String let offensivePower: Int let additionalEffect: String? }暗黙的にアンラップされたOptional型
下記のコードを見ると変数の定義時に!を指定することでアンラップが暗黙的に行われています。
しかし初期化されていない値に対してアクセスしたため実行時エラーになっています。
このように暗黙的なアンラップは危険な面があります。しかし、初期化時に値が決まっていなくても、初期化以降に必ず値が設定される場合は用いたほうが、可読性も上がり効率的です。class Sample { var value: String! } let sample = Sample() sample.value //実行時エラー以下が利用した方が良い例です。
このコードはStoryboard上に配置した要素から自動的に生成されるコードで、Storyboard上の要素と@IBOutlet属性とともに宣言されたプロパティと紐づけています。
このsomeLabelプロパティはインスタンス化される際には空ですが、その後にStoryboardから生成された値が設定されるので、暗黙的にアンラップして大丈夫です。
もしここでアンラップしなければオプショナルバインディングを別で行わなければならないため、そちらはコードを冗長にしないためにも止めておきましょう。@IBOutlet weak var someLabel: UILabel!
- 投稿日:2019-10-21T21:32:53+09:00
UITableViewをUIStackView + UIStackViewに置き換える
UITableViewにCustomCellゴテゴテで実装された画面を、UIScrollViewに置き換える機会がありましたので共有します。
主にUIStackViewへの追加で制約が入らなくてつまりました。※UITableViewCellをそのまま使う前提で書いてます。
普通にViewを置く場合はもっと簡単にできると思います。前説
まずUITableViewCellってコードから生成できるの…?
というところから始まりました。
途中、
制約全然効いてくれない!!
セルの高さが全部同じになっちゃう!!
横幅がおかしくなる!!!
とか色々やりながら、結果生まれたのが以下のコードです。その結果生まれたのが以下のコードです。
コードととストーリーボードの例
ViewController.swiftimport UIKit class ViewController: UIViewController { @IBOutlet weak var stack: UIStackView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. let row = 20 for i in 0...row { let cell = Cell.init(style: .default, reuseIdentifier: "Cell", widht: self.stack.frame.width) cell.heightAnchor.constraint(equalToConstant: cell.frame.height).isActive = true cell.hogeLabel.text = "\(i)" print("add to stack:\(cell)") self.stack.addArrangedSubview(cell) } } }Cell.swiftimport UIKit class Cell:UITableViewCell { // セルにはラベル1つだけおいてある @IBOutlet weak var hogeLabel: UILabel! // ストーリーボードで配置した時の初期化処理※ストーリーボードからの初期化は想定してません required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.nibInit(width: 0.0) } // 横幅を受け取って初期化する(制約がうまく効かなかったので) init(style: UITableViewCell.CellStyle, reuseIdentifier: String?, widht:CGFloat) { super.init(style: style, reuseIdentifier: reuseIdentifier) self.nibInit(width: widht) } // viewの初期化 fileprivate func nibInit(width: CGFloat) { guard let view = UINib(nibName: "Cell", bundle: nil) .instantiate(withOwner: self, options: nil) .first as? UIView else { return } //セルの高さはセル側でランダムに決める※使うときはちゃんとConstとか作って高さ設定してね let height = Bool.random() ? 50:100 view.frame = CGRect(x: 0, y: 0, width: width, height: CGFloat(height)) self.frame = CGRect(x: 0, y: 0, width: width, height: CGFloat(height)) self.addSubview(view) } }ストーリーボードは特に特別な設定はしておりません。
普通にScrollViewをおいて中にStackViewを配置するだけです。
もっとこんな方法あるよ!!や、
ここちょっとやばいよ!!などございましたらお教えいただけますと幸いです。ありがとうございました。
- 投稿日:2019-10-21T21:32:53+09:00
UITableViewをUIScrollView + UIStackViewに置き換える
UITableViewにCustomCellゴテゴテで実装された画面を、UIScrollViewに置き換える機会がありましたので共有します。
主にUIStackViewへの追加で制約が入らなくてつまりました。※UITableViewCellをそのまま使う前提で書いてます。
普通にViewを置く場合はもっと簡単にできると思います。前説
まずUITableViewCellってコードから生成できるの…?
というところから始まりました。
途中、
制約全然効いてくれない!!
セルの高さが全部同じになっちゃう!!
横幅がおかしくなる!!!
とか色々やりながら、結果生まれたのが以下のコードです。その結果生まれたのが以下のコードです。
コードとストーリーボードの例
ViewController.swiftimport UIKit class ViewController: UIViewController { @IBOutlet weak var stack: UIStackView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. let row = 20 for i in 0...row { let cell = Cell.init(style: .default, reuseIdentifier: "Cell", widht: self.stack.frame.width) cell.heightAnchor.constraint(equalToConstant: cell.frame.height).isActive = true cell.hogeLabel.text = "\(i)" print("add to stack:\(cell)") self.stack.addArrangedSubview(cell) } } }Cell.swiftimport UIKit class Cell:UITableViewCell { // セルにはラベル1つだけおいてある @IBOutlet weak var hogeLabel: UILabel! // ストーリーボードで配置した時の初期化処理※ストーリーボードからの初期化は想定してません required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.nibInit(width: 0.0) } // 横幅を受け取って初期化する(制約がうまく効かなかったので) init(style: UITableViewCell.CellStyle, reuseIdentifier: String?, widht:CGFloat) { super.init(style: style, reuseIdentifier: reuseIdentifier) self.nibInit(width: widht) } // viewの初期化 fileprivate func nibInit(width: CGFloat) { guard let view = UINib(nibName: "Cell", bundle: nil) .instantiate(withOwner: self, options: nil) .first as? UIView else { return } //セルの高さはセル側でランダムに決める※使うときはちゃんとConstとか作って高さ設定してね let height = Bool.random() ? 50:100 view.frame = CGRect(x: 0, y: 0, width: width, height: CGFloat(height)) self.frame = CGRect(x: 0, y: 0, width: width, height: CGFloat(height)) self.addSubview(view) } }ストーリーボードは特に特別な設定はしておりません。
普通にScrollViewをおいて中にStackViewを配置するだけです。
もっとこんな方法あるよ!!や、
ここちょっとやばいよ!!などございましたらお教えいただけますと幸いです。ありがとうございました。
- 投稿日:2019-10-21T20:09:42+09:00
クラスの継承とプロトコルの準拠の使い分け
抽象概念の共有
各クラスや構造体に共通の抽象概念を共有する場合、クラスでは親のクラスを継承しその親の特徴を子にも共有できるようにし、プロトコルではその決まりごとを準拠して共通のルール(特徴)を他の構造体などと統一することができます。
親の特徴を受け継ぎ、子でも使えるようにするこの機能は一見同じように見えますが、実際はどのような違いがあり、どのように状況に合わせて使っていけば良いのでしょうか???
今回はこれについて考えていきます。プロトコルの準拠
Swiftの機能であるプロトコルを準拠した場合の利点について解説していきます。
まず最初にクラスで実装した際の欠点について考えていきます。以下のコードを見て、どのあたりが問題でしょうか?
問題点は2点あります。
一つ目は、インスタンス化してはいけない親クラスのFoodがインスタンス化可能であること。
二つ目は、店ではなく自然に生えているberryクラスにもshop変数が継承されているということです。それでは上記の2点を解決していくためにプロトコルを用いていきます。
class Food { var shop: String? func taste() func eat() } class Sushi : Food { override func eat() { print("dip a soy source") } } class Berry : Food { override func eat() { print("take from tree") } }以下のコードがプロトコルで実装したコードです。
親をプロトコルにしたため、誤ってインスタンス化される恐れがなくなりました。そして変数shopがSushiだけに準拠でき、必要のない要素を各構造体に入れずにすみました。また、Foodプロトコルを拡張しtasteメソッドを実装することで構造体に共通のメソッドの記述しなくてすみました。protocol Shop { var shop: String { get set} } protocol Food { func taste() func eat() } extension Food { func taste() {print("tasty!")} } struct Sushi : Food, Shop { var shop: String func eat() {print("dip a soysource")} } struct Berry : Food { func eat() {print("take from tree")} }クラスの継承
クラスの継承を用いる時は複数の型同士で同一のストアドプロパティを共有したいときに利用します。
以下のコードのように継承するクラス達が同じ持ち主(ストアドプロパティ)である場合、クラスのような参照型を利用すると、同じストアドプロパティを参照するのでこのような結果になります。このように同じ値を共有したい場合はクラスの継承を使用するべきです。一応プロトコルでも実装可能ではありますが、複雑で冗長なコードになるので使わないのが得策です。
class Item { var owner: String? { didSet { guard let owner = owner else {return} print("\(owner) has a this item." } } } class Phone : Item {} class Shoes : Item{} let phone = phone phone.owner = "Sanetugu Nekoyasiki" //実行結果 Sanetugu Nekoyasiki has a this item.
- 投稿日:2019-10-21T20:09:42+09:00
【Swift】クラスの継承とプロトコルの準拠の使い分け
抽象概念の共有
各クラスや構造体に共通の抽象概念を共有する場合、クラスでは親のクラスを継承しその親の特徴を子にも共有できるようにし、プロトコルではその決まりごとを準拠して共通のルール(特徴)を他の構造体などと統一することができます。
親の特徴を受け継ぎ、子でも使えるようにするこの機能は一見同じように見えますが、実際はどのような違いがあり、どのように状況に合わせて使っていけば良いのでしょうか???
今回はこれについて考えていきます。プロトコルの準拠
Swiftの機能であるプロトコルを準拠した場合の利点について解説していきます。
まず最初にクラスで実装した際の欠点について考えていきます。以下のコードを見て、どのあたりが問題でしょうか?
問題点は2点あります。
一つ目は、インスタンス化してはいけない親クラスのFoodがインスタンス化可能であること。
二つ目は、店ではなく自然に生えているberryクラスにもshop変数が継承されているということです。それでは上記の2点を解決していくためにプロトコルを用いていきます。
class Food { var shop: String? func taste() func eat() } class Sushi : Food { override func eat() { print("dip a soy source") } } class Berry : Food { override func eat() { print("take from tree") } }以下のコードがプロトコルで実装したコードです。
親をプロトコルにしたため、誤ってインスタンス化される恐れがなくなりました。そして変数shopがSushiだけに準拠でき、必要のない要素を各構造体に入れずにすみました。また、Foodプロトコルを拡張しtasteメソッドを実装することで構造体に共通のメソッドの記述しなくてすみました。protocol Shop { var shop: String { get set} } protocol Food { func taste() func eat() } extension Food { func taste() {print("tasty!")} } struct Sushi : Food, Shop { var shop: String func eat() {print("dip a soysource")} } struct Berry : Food { func eat() {print("take from tree")} }クラスの継承
クラスの継承を用いる時は複数の型同士で同一のストアドプロパティを共有したいときに利用します。
以下のコードのように継承するクラス達が同じ持ち主(ストアドプロパティ)である場合、クラスのような参照型を利用すると、同じストアドプロパティを参照するのでこのような結果になります。このように同じ値を共有したい場合はクラスの継承を使用するべきです。一応プロトコルでも実装可能ではありますが、複雑で冗長なコードになるので使わないのが得策です。
class Item { var owner: String? { didSet { guard let owner = owner else {return} print("\(owner) has a this item." } } } class Phone : Item {} class Shoes : Item{} let phone = phone phone.owner = "Sanetugu Nekoyasiki" //実行結果 Sanetugu Nekoyasiki has a this item.
- 投稿日:2019-10-21T18:54:38+09:00
[Swift] カメラロールから取得した動画のトリミング
カメラロールから取得した動画のトリミング方法
調べた記事を見ていると、
UIImagePickerControllerとUIVideoEditorControllerを用いてやる的なことが書いてあったのだけど、実はUIImagePickerControllerだけでできるとのこと、、、(参考記事が少なすぎる?)
とりあえずどちらでもできるようにしたので、どっちも書きます。。。
どちらも
iOS13.1.2で動作確認ずみUIImagePickerControllerとUIVideoEditorControllerを用いてカメラロールから取得した動画トリミング
いらない記述があるかもしれない。
参考記事 https://github.com/albertbori/iOS-Sample-Video-Trimmerimport UIKit import Photos import MobileCoreServices //省略 func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { //動画のとき if #available(iOS 11.0, *) { if let asset = info[.phAsset] as? PHAsset { let options = PHVideoRequestOptions() options.isNetworkAccessAllowed = true let manager = PHImageManager.default() manager.requestAVAsset(forVideo: asset, options: options) {asset, audioMix, info in guard let asset = asset else { print("asset is nil") return } if let assetUrl = asset as? AVURLAsset { let tempFolderUrl = URL(fileURLWithPath: NSTemporaryDirectory()).standardizedFileURL let asset = AVAsset(url: assetUrl.url) let outputUrl = tempFolderUrl.appendingPathComponent(assetUrl.url.lastPathComponent) guard let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else { print("次のビデオをエクスポートできませんでした: \(assetUrl.url)") return } exporter.outputURL = outputUrl exporter.outputFileType = AVFileType.mp4 exporter.exportAsynchronously { DispatchQueue.main.async { picker.dismiss(animated: true, completion: nil) } self.showEditor(for: outputUrl) } } } } } } func mimeTypeForPath(path: String) -> String { let url = NSURL(fileURLWithPath: path) let pathExtension = url.pathExtension if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension! as NSString, nil)?.takeRetainedValue() { if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { return mimetype as String } } return "application/octet-stream" } func showEditor(for outputUrl: URL) { guard UIVideoEditorController.canEditVideo(atPath: outputUrl.path) else { print("ビデオは編集できません: \(outputUrl.path)") return } DispatchQueue.main.async { let vc = UIVideoEditorController() vc.videoPath = outputUrl.path vc.videoMaximumDuration = 15 vc.videoQuality = UIImagePickerController.QualityType.typeIFrame960x540 vc.delegate = self self.present(vc, animated: true, completion: nil) } } static func deleteAsset(at path: String) { do { try FileManager.default.removeItem(at: URL(fileURLWithPath: path)) print("Deleted asset file at: \(path)") } catch { print("Failed to delete assete file at: \(path).") print("\(error)") } } //ビデオのURLからサムネイル画像を作成 func previewImageFromVideo(_ url:URL) -> UIImage? { print("動画からサムネイルを生成(URL)") let asset = AVAsset(url: url) let imageGenerator = AVAssetImageGenerator(asset: asset) imageGenerator.appliesPreferredTrackTransform = true var time = asset.duration time.value = min(time.value, 2) do { let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil) return UIImage(cgImage: imageRef) } catch { print(error) //エラーを黙って捨ててしまってはいけない return nil } } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true, completion: nil) } } extension GameEditVC: UIVideoEditorControllerDelegate { //動画トリミング成功時 func videoEditorController(_ editor: UIVideoEditorController, didSaveEditedVideoToPath editedVideoPath: String) { //エラーで2回これが行われるので、こういった処理をする if editorBool == false { editorBool = true return } else { editorBool = false print("このパスに保存完了: \(editedVideoPath)") DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { GameEditVC.deleteAsset(at: editor.videoPath) } let videoURL = URL(fileURLWithPath: editedVideoPath) print(videoURL) //<-危険な強制アンラップは可能な限り避ける guard let image = previewImageFromVideo(videoURL) else { print("previewImageFromVideo(\(videoURL)) is nil") return } self.profileImages.append(image) DispatchQueue.main.async { self.collectionView.reloadData() } editor.dismiss(animated:true, completion: nil) } } // 動画トリミング失敗時の処理 private func videoEditorController(editor: UIVideoEditorController, didFailWithError error: NSError) { print("an error occurred: \(error.localizedDescription)") dismiss(animated:true) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { GameEditVC.deleteAsset(at: editor.videoPath) } } // 動画トリミングキャンセル時の処理 func videoEditorControllerDidCancel(_ editor: UIVideoEditorController) { dismiss(animated:true) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { GameEditVC.deleteAsset(at: editor.videoPath) } } }UIImagePickerControllerのみでカメラロールから取得した動画トリミング
普通にこちらがおすすめです。
参考記事:
https://qiita.com/k_kuni/items/08e66d4d8074bff2c4a3
https://stackoverflow.com/questions/40354689/swift-how-to-record-video-in-mp4-format-with-uiimagepickercontroller/40354948#40354948またアンドロイド用に.movではなく.mp4で保存したいので、そちらもやるか、、、
そしてプラスでfirestorageに保存するところまで!!かなりいろいろ見たけど以下でできたよ、、、(まじで疲れたwww)
完成コードはこれ
import UIKit import FirebaseAuth import Firebase import FirebaseStorage import Photos import MobileCoreServices import AVFoundation //撮影完了時------------------------------- func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let url = info[UIImagePickerController.InfoKey.mediaURL] as? NSURL { self.encodeVideo(videoURL: url as URL) picker.dismiss(animated: true, completion: nil) } } func encodeVideo(videoURL: URL){ indicator.startFullIndicator(view: view) let avAsset = AVURLAsset(url: videoURL) let startDate = Date() let exportSession = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetPassthrough) let docDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] let myDocPath = NSURL(fileURLWithPath: docDir).appendingPathComponent("temp.mp4")?.absoluteString let docDir2 = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as NSURL let filePath = docDir2.appendingPathComponent("rendered-Video.mp4") deleteFile(filePath!) if FileManager.default.fileExists(atPath: myDocPath!){ do{ try FileManager.default.removeItem(atPath: myDocPath!) }catch let error{ print(error) indicator.stopFullIndicator(view: view) } } exportSession?.outputURL = filePath exportSession?.outputFileType = AVFileType.mp4 exportSession?.shouldOptimizeForNetworkUse = true let start = CMTimeMakeWithSeconds(0.0, preferredTimescale: 0) let range = CMTimeRange(start: start, duration: avAsset.duration) exportSession?.timeRange = range exportSession!.exportAsynchronously{() -> Void in switch exportSession!.status{ case .failed: print("\(exportSession!.error!)") self.indicator.stopFullIndicator(view: self.view) case .cancelled: print("Export cancelled") self.indicator.stopFullIndicator(view: self.view) case .completed: let endDate = Date() let time = endDate.timeIntervalSince(startDate) print(time) print("Successful") print(exportSession?.outputURL ?? "") let profileMovieRef = self.gameUserStorageRef.child("profileImage_\(self.profileImages.count).mp4") profileMovieRef.putFile(from: exportSession!.outputURL!, metadata: nil, completion: { (metadata, error) in if error != nil { print("エラー:\(error!)") self.indicator.stopFullIndicator(view: self.view) } else { profileMovieRef.downloadURL { (url, error) in guard let downloadURL = url else { print("エラー:\(error!)") self.indicator.stopFullIndicator(view: self.view) return } self.indicator.stopFullIndicator(view: self.view) } } } ) default: break } } } func deleteFile(_ filePath:URL) { guard FileManager.default.fileExists(atPath: filePath.path) else{ return } do { try FileManager.default.removeItem(atPath: filePath.path) }catch{ fatalError("Unable to delete file: \(error) : \(#function).") } } //ビデオのURLからサムネイル画像を作成 func previewImageFromVideo(_ url:URL) -> UIImage? { print("動画からサムネイルを生成(URL)") let asset = AVAsset(url: url) let imageGenerator = AVAssetImageGenerator(asset: asset) imageGenerator.appliesPreferredTrackTransform = true var time = asset.duration time.value = min(time.value, 2) do { let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil) return UIImage(cgImage: imageRef) } catch { print(error) //エラーを黙って捨ててしまってはいけない return nil } } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true, completion: nil) } func showAddAlertController() { let alertController = UIAlertController(title: "選択してください", message: "画像・動画を追加します", preferredStyle: .actionSheet) let albumButton = UIAlertAction(title: "画像を選択", style: .default) { (action: UIAlertAction!) in self.indicator.startFullIndicator(view: self.view) let sourceType:UIImagePickerController.SourceType = .photoLibrary if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.photoLibrary){ let cameraPicker = UIImagePickerController() cameraPicker.sourceType = sourceType cameraPicker.mediaTypes = ["public.image"] cameraPicker.delegate = self self.present(cameraPicker, animated: true, completion: nil) self.indicator.stopFullIndicator(view: self.view) } } let movieButton = UIAlertAction(title: "動画を選択", style: .default) { (action: UIAlertAction!) in let sourceType:UIImagePickerController.SourceType = .photoLibrary if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.photoLibrary){ let controller = UIImagePickerController() controller.sourceType = sourceType controller.mediaTypes=[kUTTypeMovie as String] // 動画のみ controller.delegate = self controller.allowsEditing = true controller.videoMaximumDuration = 10 // 10秒で動画を切り取る controller.videoQuality = UIImagePickerController.QualityType.typeMedium self.present(controller, animated: true, completion: nil) } } let cancelButton = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil) alertController.addAction(albumButton) alertController.addAction(movieButton) alertController.addAction(cancelButton) //alertControllerを表示させる self.present(alertController, animated: true, completion: nil) }
- 投稿日:2019-10-21T15:58:05+09:00
UIScrollVIewの上でも、キーボードが現れたときにいい感じにスクロールする
備忘録兼紹介です。
UIScrollView × 複数のUITextFieldで、キーボードが出たときにスクロールさせる処理を紹介します。動き
コード
import UIKit class ViewController: UIViewController { @IBOutlet weak var scroll: UIScrollView! // UITextFieldとかを配置するView @IBOutlet weak var stack: UIStackView! // 表示サンプル用 // ScrollViewのBottomの制約 @IBOutlet weak var scrollViewBottomConstraints: NSLayoutConstraint! private var nowEditingField:UITextField! // 編集中のUITextField private var offsetAdded:CGFloat? // スクロール戻す時用 override func viewDidLoad() { super.viewDidLoad() // 表示サンプルとして、UILabelを20個配置 let lines = 20 for count in 1...lines { let field = UITextField() field.placeholder = "\(count)" field.borderStyle = .roundedRect field.textAlignment = .center field.delegate = self // ← UITextFieldのdelegateを拾いたいので、delegateを設定する self.stack.addArrangedSubview(field) field.widthAnchor.constraint(equalToConstant: 250.0).isActive = true } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // キーボードが表示されるときと消えるときに動作させるメソッドを登録 // キーボードの高さがほしかった NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil ) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) NotificationCenter.default.removeObserver(UIResponder.keyboardWillShowNotification) } } extension ViewController: UITextFieldDelegate { // テキストフィールドにフォーカスがあたった時に呼ばれる func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { self.nowEditingField = textField return true } // テキストフィールドからフォーカスが外れる時に呼ばれる func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { self.nowEditingField = nil let offsetAdded = self.offsetAdded self.offsetAdded = nil var offset:CGPoint = CGPoint(x: 0, y: 0) if self.scroll.contentOffset.y - (offsetAdded ?? 0) < 0 { // 上端 offset = CGPoint(x: self.scroll.contentOffset.x, y: 0) }else if self.scroll.contentOffset.y > (self.scroll.contentSize.height - self.scroll.frame.size.height - self.scrollViewBottomConstraints.constant) { // 下端 offset = CGPoint(x: self.scroll.contentOffset.x, y: self.scroll.contentOffset.y) }else { // その他 offset = CGPoint(x: self.scroll.contentOffset.x, y: self.scroll.contentOffset.y - (offsetAdded ?? 0)) } self.scrollViewBottomConstraints.constant = 0 self.scroll.setContentOffset(offset, animated: true) return true } // キーボードが表示される直前に呼ばれる ※textFieldShouldBeginEditingより後に呼ばれる @objc func keyboardWillShow(_ notification: Notification) { guard let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } guard let field = self.nowEditingField else { return } let keyboardHeight = keyboardFrame.cgRectValue.height let keyboardTopLine = self.view.frame.height - keyboardHeight let bottomLine = field.frame.origin.y + field.frame.height let displayBottom = bottomLine - self.scroll.contentOffset.y if displayBottom > keyboardTopLine { let sub = displayBottom - keyboardTopLine self.offsetAdded = sub let offset = CGPoint(x: self.scroll.contentOffset.x, y: self.scroll.contentOffset.y + sub) self.scroll.setContentOffset(offset, animated: true) self.scrollViewBottomConstraints.constant = keyboardHeight } } // キーボードのリターンでキーボードを閉じる func textFieldShouldReturn(_ textField: UITextField) -> Bool { self.view.endEditing(true) return true } }
ありがとうございました。
よかったら使ってください。(もっといい実装があれば教えて下さい。)
- 投稿日:2019-10-21T15:42:36+09:00
UILabelが省略されているか判定する
意外と見つからなくて困ったので、自分なりの実装方法を共有します。
コード
extension UILabel { var isOverflowing:Bool { return (realLineCount() > self.numberOfLines) } private func realLineCount()-> Int { guard let font = self.font else { return 0 } guard let text = self.text, text != "" else { return 0 } let sizeForWidthCheck = CGSize(width: Int.max, height: Int(ceil(font.pointSize))) let oneLineWidth = text.boundingRect(with: sizeForWidthCheck, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil).width let boundingWidth = text.boundingRect(with: self.bounds.size, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil).width return Int(ceil(oneLineWidth / boundingWidth)) } }解説
UILabel.textは省略されている場合でも全文を返してくれるので、それを使用します。
realLineCount
メソッドで、省略せずに全文を描画したときの行数を計算します。let oneLineWidth = text.boundingRect(with: sizeForWidthCheck, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil).widthboundingRectメソッドはStringを描画した場合のRectを返してくれます。
これを使って、1行で全部描画したときの横幅を計算します。1return Int(ceil(oneLineWidth / boundingWidth))それを
boundingWidth
(Labelに描画したときの横幅)で割って、行数を計算します。2
小数点以下はいらないので、繰り上げます。var isOverflowing:Bool { return (realLineCount() > self.numberOfLines) }計算した行数を自身の行数(ここは実際に表示される行数)と比較し、
大きければ省略されているのでtrue
を返します。純正で用意して欲しいな……
ありがとうございました。
- 投稿日:2019-10-21T12:02:10+09:00
UIButtonのTitleを中央揃えにする
UIButtonのTitleはデフォルトで中央揃えですが、なぜか左に寄ってしまうバグがあったのでコード上で中央揃えに設定しました。
環境
- Xcode 11.0
- Swift 5.1
ボタンのタイトル中央揃え
hogeButton.titleLabel?.textAlignment = NSTextAlignment.center
で設定。ボタンのタイトル上下中央揃え
上下の位置も、下に寄ってしまっていたのでプロパティで設定。
hogeButton.titleLabel?.baselineAdjustment = .alignCenters
おまけ
その他のよく使うボタンタイトルに関するプロパティをまとめました。
(ボタンのタイトルはUILabelなので、UILabelのプロパティを使っているだけですが)//LabelやButtonのサイズに合わせてフォントサイズを可変にする hogeButton.titleLabel?.adjustsFontSizeToFitWidth = true label.numberOfLines = 1 //lineBreakModeの設定 //表示できる部分のみを切り取る hogeButton.titleLabel?.lineBreakMode = .byClipping //複数行の場合、文字単位で折り返す hogeButton.titleLabel?.lineBreakMode = .byCharWrapping //複数行の場合、単語単位で折り返す hogeButton.titleLabel?.lineBreakMode = .byWordWrapping //文末を...で省略する hogeButton.titleLabel?.lineBreakMode = .byTruncatingTail参考
- 投稿日:2019-10-21T08:51:04+09:00
Swift の非同期処理の例(複数パターン)
はじめに
こんにちは! iOSエンジニアのやまたつ です☺️
Oshidoriというアプリを個人で開発し、リリースしています!!!Oshidoriというアプリを個人で開発し、リリースしています!!!非同期処理を書く方法っていくつかあるんだなあと思ってまとめてみたくなったので書きました!
実務だったり、個人アプリで使った非同期処理のパターンを書きたいと思います。
複数通信の時の処理も書きます。Promise Kit が有能すぎて感動したので Promise Kit 贔屓になってます。ご了承ください。
クロージャを使って非同期処理を書く
例
hoge.swift// ここがクロージャ //////////////////// func fetchUser(id: Int, comletion: @escaping (User?,Error?)->()) { // ここで通信 uesrInfoRep.fetch(id) { user, error in // 通信終わったら入ってくる guard let _ = error else { // エラーの時は nil と、 errorを返す comletion(nil, error) return } // ちゃんと値が返ってきたら user と nil を返す completion(user, nil) } } // こんな風に使う // completionの引数が使えるようになる fetchUser() { user, error in guard let _ = error else { // エラー処理 return } self.user = user tableView.reload() }メリット
- 慣れたらお手軽にかける
デメリット
- 処理が複雑になってネストすると追うのが辛い
本当にあった怖い話
半年前くらいに書いたコードを見返したらこれが出てきました。
つっこみどころも満載だし、ネストが深すぎて修正する気すらなくすコード。
これがダメなコードってわかるくらいには成長していました。。。
一番怖いのが、getAllInfo を呼び出しているところで completion の処理を何も書いていないこと。。。func getAllInfo(messageId: String, completion: @escaping () -> ()) { userInfoRep.getUserInfo { (userInfo) in self.userInfo = userInfo self.roomRep.getRoomInfo(roomId: userInfo.roomId, completion: { (room) in self.roomInfo = room if userInfo.partnerId == room.partnerId { var imageUrl: URL? imageUrl = URL(string: room.partnerImageUrl) if let url = imageUrl { let tmpImageView = UIImageView() Nuke.loadImage(with: url, options: ImageLoadingOptions( placeholder: UIImage(named: "Oshidori_null"), transition: .fadeIn(duration: 0.33) ), into: tmpImageView, progress: { (response, tmp , tmp1) in // 呼び出し元はこうなっていた(恐怖) messageRoomService.getAllInfo(messageId: messageId) {}DispatchGroup を使う
コメントにどのような処理か書いています。
func fetchEntryDetail(entry: Entry, completion: @escaping (Offer?, User?, Error?) -> Void){ var offer: Offer! var user: User! // 非同期のグループ作るよ!!! let dispatchGroup = DispatchGroup() // 並列で実行できるよ〜 let dispatchQueue = DispatchQueue(label: "queue", attributes: .concurrent) // 1つ目の並列処理入ります! dispatchGroup.enter() dispatchQueue.async { entry.offerRef.getDocument { (ss, err) in guard err == nil else { completion(nil, nil, err); return } guard let ss = ss else { completion(nil, nil, nil); return } guard let data = ss.data() else { completion(nil, nil, nil); return } offer = Offer(id: ss.documentID, document: data, entryStatus: .entried ) // 1つ目終わりました〜 dispatchGroup.leave() } } // 2つ目の並列処理入ります! dispatchGroup.enter() dispatchQueue.async { entry.userRef.getDocument { (ss, err) in guard err == nil else { completion(nil, nil, err); return } guard let ss = ss else { completion(nil, nil, nil); return } guard let data = ss.data() else { completion(nil, nil, nil); return } user = User(id: ss.documentID, dict: data) // 2つ目終わりました〜 dispatchGroup.leave() } } // 上の二つの処理が終わった時(両方の dispatchGroup.leave() が呼ばれた時)実行される dispatchGroup.notify(queue: .main) { completion(offer, user, nil) } }メリット
- 複数処理にはむいていそう
デメリット
- 書き方を覚えるのが少し辛い
- leave し忘れたりすると、意図しない処理になったりする。気付きにくそう。
以下の記事を読みながら実務でコードを書きました!
わかりやすいです!
Swiftで複数の非同期処理の完了時に処理を行う
Promise Kit
スターも 12,000 もあり、最新更新も1ヶ月前ととても活発なライブラリです!
使ってみた感じとても良かったのでオススメです!!!
こちらドキュメントです!こんなにネストだらけのコードが、
login { creds, error in if let creds = creds { fetch(avatar: creds.user) { image, error in if let image = image { self.imageView = image } } } }綺麗にかける!!!!
// 最初に firstly { // ログインして login() // ログインが終わったら }.then { creds in // ログインの結果取得した creds を使う fetch(avatar: creds.user) }.done { image in // fetch が終わったら、fetchしたimageを使える self.imageView = image }Promise Kit スゲ〜〜〜
複数通信もお手の物
クロージャだと
// これが終わったら operation1 { result1 in // 2をやる operation2 { result2 in // 2が終わったらこれ。ネスト。。。 finish(result1, result2) } }Dispatchだと
やっぱり少し複雑
var result1: …! var result2: …! let group = DispatchGroup() group.enter() group.enter() operation1 { result1 = $0 group.leave() } operation2 { result2 = $0 group.leave() } group.notify(queue: .main) { finish(result1, result2) }PromiseKit だと
firstly { when(fulfilled: operation1(), operation2()) }.done { result1, result2 in //… }PromiseKit 最高〜〜〜☺️
最後に
Promise Kit とても良いライブラリなのでぜひ使ってみてください!!!
OSS開発に興味が出てきました。
PR出したりして貢献してみたいですね!!!番外編
RxSwift 公式ドキュメント (RxSwiftできれば解決なんですけどね笑)
RxSwift入門(2) 非同期処理してみる
RxSwift は業務で使っていますが、完璧に理解できているには遠く及ばず。。。
きちんと勉強して、「RxSwift 最高〜〜〜☺️」 となる日が早く訪れるようにします。