- 投稿日:2020-02-21T23:29:25+09:00
AutoLayoutの競合??
はじめに
AutoLayoutはきちんと設定したのに、デバッグエリアをみると以下のようなメッセージが書かれていることはありませんか??
2020-02-21 22:56:34.775265+0900 TableView[13144:2779148] [LayoutConstraints] Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. ( "<NSLayoutConstraint:0x2807d8460 UILabel:0x104a1ae70'\U8679\U8272'.height == 32 (active)>", "<NSLayoutConstraint:0x2807d28a0 UIStackView:0x107203e70.height == 500 (active)>", "<NSLayoutConstraint:0x2807e80f0 UILayoutGuide:0x281de0000'UIViewSafeAreaLayoutGuide'.bottom == UIStackView:0x107203e70.bottom (active)>", "<NSLayoutConstraint:0x2807e82d0 UIStackView:0x107203e70.top == UILayoutGuide:0x281de0000'UIViewSafeAreaLayoutGuide'.top (active)>", "<NSLayoutConstraint:0x2807d8780 V:|-(0)-[UIStackView:0x104a0f890] (active, names: '|':UITableViewCellContentView:0x104a1c810 )>", "<NSLayoutConstraint:0x2807d8820 V:[UIStackView:0x104a0f890]-(0)-| (active, names: '|':UITableViewCellContentView:0x104a1c810 )>", "<NSLayoutConstraint:0x2807dc7d0 'UISV-canvas-connection' UIStackView:0x104a1d4e0.top == TableView.RainbowColorView:0x107203d00.top (active)>", "<NSLayoutConstraint:0x2807dc820 'UISV-canvas-connection' V:[TableView.RainbowColorView:0x107203d00]-(0)-| (active, names: '|':UIStackView:0x104a1d4e0 )>", "<NSLayoutConstraint:0x2807d9310 'UISV-canvas-connection' UIStackView:0x104a0f890.top == UILabel:0x104a1ae70'\U8679\U8272'.top (active)>", "<NSLayoutConstraint:0x2807dc910 'UISV-canvas-connection' V:[UIStackView:0x104a1d4e0]-(0)-| (active, names: '|':UIStackView:0x104a0f890 )>", "<NSLayoutConstraint:0x2807d93b0 'UISV-spacing' V:[UILabel:0x104a1ae70'\U8679\U8272']-(0)-[UIButton:0x104a1cbb0'\U3053\U3053\U3092\U30bf\U30c3\U30d7\U3057\U3066\U304f\U3060\U3055\U3044'] (active)>", "<NSLayoutConstraint:0x2807dc960 'UISV-spacing' V:[UIButton:0x104a1cbb0'\U3053\U3053\U3092\U30bf\U30c3\U30d7\U3057\U3066\U304f\U3060\U3055\U3044']-(0)-[UIStackView:0x104a1d4e0] (active)>", "<NSLayoutConstraint:0x2807d9770 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x104a1c810.height == 64 (active)>", "<NSLayoutConstraint:0x2807e8190 'UIViewSafeAreaLayoutGuide-bottom' V:[UILayoutGuide:0x281de0000'UIViewSafeAreaLayoutGuide']-(0)-| (active, names: '|':TableView.RainbowColorView:0x107203d00 )>", "<NSLayoutConstraint:0x2807e8050 'UIViewSafeAreaLayoutGuide-top' V:|-(0)-[UILayoutGuide:0x281de0000'UIViewSafeAreaLayoutGuide'] (active, names: '|':TableView.RainbowColorView:0x107203d00 )>" ) Will attempt to recover by breaking constraint <NSLayoutConstraint:0x2807d28a0 UIStackView:0x107203e70.height == 500 (active)> Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.もしかしたら、制約が競合している可能性があるかもしれません。今回はその場合の、
- 原因
- 対処法
の2点に絞って説明していきます!
※説明が間違っていたら申し訳ありません、、、環境
- macOS Catalina(10.15.2)
- xcode(11.3)
- Swift5.1
原因
ボタンタップ前 ボタンタップ後 今回の場合、ボタンをタップすると虹色が表示され、その途端に上記のメッセージが表示されました。
※虹色はUIStackViewとUILabelで管理しており、UIStackViewのheight = 500で設定しています。デフォルトでUIStackViewをisHidden = trueで設定しています。そのUIStackViewをUITableViewCellが管理しているという構造です。
上記のメッセージを読むと、下の方にWill attempt to recover by breaking constraint <NSLayoutConstraint:0x283fe4910 UIStackView:0x10f901490.height == 500 (active)>と書かれています。読んでみると、UIStackViewのheight = 500の制約が原因みたいです。もう少し詳しく説明すると、UIStackViewを非表示から表示にすることでUITableViewCellの高さが再計算され、その高さとUIStackViewのheight = 500を同時に満たそうとしていることが原因みたいです。
対処法
方法は至って単純です。UIStackViewのheight = 500の制約のPriorityを下げるだけで解決します。
これにより、制約の競合が発生した場合にUIStackViewのheight = 500の優先順位が下がります。基本的にはこの辺のこともxcodeが計算してくれますが、不具合の原因にもなりかねないのできちんと対応したいところですね笑
参考
- 投稿日:2020-02-21T17:14:03+09:00
XcodeのProject間でUIコピペする方法
- 投稿日:2020-02-21T16:20:17+09:00
UserDefaultsの中身全てを雑に出力する
Swift5.0。
デバッグしていて、「あーとりあえずUserDefaultsの中身ドバッと出したいな……」と思いました。
UserDefaultsから普通に取り出そうとすると、Key名が必要となりますが、mapみたいなのないかな〜と思ったら、下記がありました。雑に出力print(UserDefaults.standard.dictionaryRepresentation())lldbデバッガのpoコマンドでやるのも手です。
出力結果は載せません。
とにかく汚いですが、全部出ます。辞書型。
自分のアプリドメイン外のものも出てる? 気がしますが、とにかく全部出ます。アプリ内で接頭詞持ってて、キー名は必ずそれが付ける運用になっていたら、下記のようにしぼると捗りそうです。
キー名で絞るprint(UserDefaults.standard.dictionaryRepresentation().filter { $0.key.hasPrefix("hoge") })
- 投稿日:2020-02-21T09:59:27+09:00
【Swift】TableViewCellにImageViewを追加してURLで指定された画像を表示する
概要
この記事は初心者の自分がRESTful なAPIとswiftでiPhone向けのクーポン配信サービスを開発した手順を順番に記事にしています。技術要素を1つずつ調べながら実装したため、とても遠回りな実装となっています。
前回の でクーポンの画像も配信するAPIを作りました。
アプリ側もAPI側から取得した画像を画面に表示できるように改造します。参考
- やさしくはじめる iPhoneアプリ作りの教科書 森 巧尚 著 マイナビ
- 【Swift入門 文法編】型キャスト(as, as!, as?)をマスターしよう
- 【Swift4】UIImageでURLで画像を指定する
環境
Mac OS 10.15
Swift5
Xcode11.1手順
- TableViewCellにUIImageViewを追加する
- 追加したUIImageViewでとりあえず画像を表示させる
- jsonで取得したURLを使って画像を表示させる
- 動作確認
TableViewCellにUIImageViewを追加する
Xcodeを開いてMain.storyboardでTableVIewCellにUIImageViewを追加します。合わせてデザインを修正してこれまでのクーポンの文字情報の下に画像を表示するようにします。
TableViewCellの Row Height を200pxから400pxに変更して縦を長くします。そして長くした部分にUIImageViewを追加します。Autoresizingで四隅を固定します。そして追加したUIImageViewに tag を付けます(重要)。Labelで4番まで使っているので、5番を割り当てました。
次にViewController.swiftを開いて、tableViewの高さを指定している
tableView.rowHeight = [数字]
の部分を Main.storyboardで指定したTableViewCellの Row Height の値に変更します。tableView.rowHeight = 400 // Row Heightで指定した長さに変更追加したUIImageViewでとりあえず画像を表示させる
ViewController.swift の tableView にUIImageViewへ画像を表示する処理を追加します。下記の通り追加しました。基本はLabelに値を設定する方法を同じです。
上で作った画像保存用のフォルダにサンプル画像を格納し、画像ファイルのフルパスをとりあえずベタ書きで指定しました。
// クーポン画像を表示 let couponImageView = cell.viewWithTag(5) as! UIImageView let imageFileUrl = "/Users/XXXXX/workspace/amiApp/images/coupon-image-001.png" couponImageView.image = UIImage(named: imageFileUrl)変更を保存してXcodeでアプリを実行すると、クーポン情報の下に画像が表示されました!
画像ファイルのパスをベタ書きしているので、全てのクーポンに同じ画像が表示されていると思います。また、少しレイアウトが崩れているので後で修正しようと思います。
レスポンスのjsonで取得したURLを使って画像を表示させる
TableViewCell や UIImageView の書き方は問題無いことが分かったので、ローカルフォルダのパスではなくAPIのレスポンスで取得した画像ファイルのURLを使って画像を取得するように変更します。
URLの取得から表示までのコードはこちらのようになります。
// URLで指定されたクーポン画像を表示 let couponImageView = cell.viewWithTag(5) as! UIImageView if let urlString = coupon["image"] as? String { let url = URL(string: urlString) do{ let imageData = try Data(contentsOf: url!) couponImageView.image = UIImage(data: imageData) } catch { print("Error : Cat't get image") } } else { couponImageView.image = UIImage(named: "no-coupon-image.png") //nilの場合は固定画像表示 } return cell }ポイントが幾つかあります。
1つ目は
if let urlString = coupon["image"] as? String { 〜
の部分です。レスポンスに画像ファイルのURLが指定されていない場合に NULL参照でアプリが落ちるのを防ぐため、as?
を使ったキャストとオプショナルバインディングを組み合わせて、画像ファイルのURLが指定されていない場合は代わりの画像を表示するようにしています。
as?
はキャストする際、指定した型ならそのオプショナル型(nilが含まれるかもしれない)を返し、指定した型以外なら nilを返します。オプショナルバインディングは下記のような書式となり、[オプショナル型の変数]が nilで無い場合に限り任意の処理を実行される事が出来ます。elseと組み合わせる事で、nilかnilで無いかで処理を分ける事も出来ます。
If let [任意の定数] = [オプショナル型の変数] { オプショナル型の変数がnilじゃなかった場合に実施したい処理 }ポイントの2つ目は
let imageData = try Data(contentsOf: url!)
の部分です。URLを使って画像を取ってくる処理です。画像を取得出来た場合はcouponImageView.image = UIImage(data: imageData)
でImageViewに画像を表示させています。動作確認
変更を保存してXcodeでアプリを実行すると、クーポン情報の下に画像が表示されました! URLを設定していない2枚目のクーポンからはアプリ側で持たせた仮の画像が表示されています。また、シミュレータを iPhone 11 Pro Max から iPhone 11 Pro にしたらレイアウトがピッタリはまりました。
これで一通りクーポンアプリの実装ができたので。次は作ったAPIをクラウドのサーバで公開させようと思います。
- 投稿日:2020-02-21T01:48:47+09:00
GoogleCloudVisionAPIのサンプルをSwift5で書き直す
Google Cloud VisionのサンプルコードのレポジトリにはiOS用のサンプルが存在するのですが、これがまた最終更新日が3年前と古く、そのまま使おうとしてもビルドできないため書き直す必要がでてきます。
Swift5で書き直した場合
SwiftyJSON
によるパースがdo-catch
に対応したのでそれ用に書き直すのが主でしょうか。
他はインターフェースがちょっと変わった程度です。
正直、Swift4でも動く気はする。// Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import UIKit import SwiftyJSON class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { let imagePicker = UIImagePickerController() let session = URLSession.shared @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var spinner: UIActivityIndicatorView! @IBOutlet weak var labelResults: UITextView! @IBOutlet weak var faceResults: UITextView! var googleAPIKey = "YOUR_API_KEY" var googleURL: URL { return URL(string: "https://vision.googleapis.com/v1/images:annotate?key=\(googleAPIKey)")! } @IBAction func loadImageButtonTapped(_ sender: UIButton) { imagePicker.allowsEditing = false imagePicker.sourceType = .photoLibrary present(imagePicker, animated: true, completion: nil) } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. imagePicker.delegate = self labelResults.isHidden = true faceResults.isHidden = true spinner.hidesWhenStopped = true } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } } /// Image processing extension ViewController { func analyzeResults(_ dataToParse: Data) { // Update UI on the main thread DispatchQueue.main.async(execute: { self.spinner.stopAnimating() self.imageView.isHidden = true self.labelResults.isHidden = false self.faceResults.isHidden = false self.faceResults.text = "" // Check for errors do { // Use SwiftyJSON to parse results let json = try JSON(data: dataToParse) // Parse the response print(json) let responses: JSON = json["responses"][0] // Get face annotations let faceAnnotations: JSON = responses["faceAnnotations"] let emotions: Array<String> = ["joy", "sorrow", "surprise", "anger"] let numPeopleDetected:Int = faceAnnotations.count self.faceResults.text = "People detected: \(numPeopleDetected)\n\nEmotions detected:\n" var emotionTotals: [String: Double] = ["sorrow": 0, "joy": 0, "surprise": 0, "anger": 0] var emotionLikelihoods: [String: Double] = ["VERY_LIKELY": 0.9, "LIKELY": 0.75, "POSSIBLE": 0.5, "UNLIKELY":0.25, "VERY_UNLIKELY": 0.0] for index in 0..<numPeopleDetected { let personData:JSON = faceAnnotations[index] // Sum all the detected emotions for emotion in emotions { let lookup = emotion + "Likelihood" let result:String = personData[lookup].stringValue emotionTotals[emotion]! += emotionLikelihoods[result]! } } // Get emotion likelihood as a % and display in UI for (emotion, total) in emotionTotals { let likelihood:Double = total / Double(numPeopleDetected) let percent: Int = Int(round(likelihood * 100)) self.faceResults.text! += "\(emotion): \(percent)%\n" } // Get label annotations let labelAnnotations: JSON = responses["labelAnnotations"] let numLabels: Int = labelAnnotations.count var labels: Array<String> = [] if numLabels > 0 { var labelResultsText:String = "Labels found: " for index in 0..<numLabels { let label = labelAnnotations[index]["description"].stringValue labels.append(label) } for label in labels { // if it's not the last item add a comma if labels[labels.count - 1] != label { labelResultsText += "\(label), " } else { labelResultsText += "\(label)" } } self.labelResults.text = labelResultsText } else { self.labelResults.text = "No labels found" } } catch { self.labelResults.text = "Error \(error)" } }) } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let pickedImage = info[.originalImage] as? UIImage { imageView.contentMode = .scaleAspectFit imageView.isHidden = true // You could optionally display the image here by setting imageView.image = pickedImage spinner.startAnimating() faceResults.isHidden = true labelResults.isHidden = true // Base64 encode the image and create the request let binaryImageData = base64EncodeImage(pickedImage) createRequest(with: binaryImageData) } dismiss(animated: true, completion: nil) } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { dismiss(animated: true, completion: nil) } func resizeImage(_ imageSize: CGSize, image: UIImage) -> Data { UIGraphicsBeginImageContext(imageSize) image.draw(in: CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height)) let newImage = UIGraphicsGetImageFromCurrentImageContext() let resizedImage = newImage!.pngData() UIGraphicsEndImageContext() return resizedImage! } } /// Networking extension ViewController { func base64EncodeImage(_ image: UIImage) -> String { var imagedata = image.pngData() // Resize the image if it exceeds the 2MB API limit if (imagedata?.count > 2097152) { let oldSize: CGSize = image.size let newSize: CGSize = CGSize(width: 800, height: oldSize.height / oldSize.width * 800) imagedata = resizeImage(newSize, image: image) } return imagedata!.base64EncodedString(options: .endLineWithCarriageReturn) } func createRequest(with imageBase64: String) { // Create our request URL var request = URLRequest(url: googleURL) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(Bundle.main.bundleIdentifier ?? "", forHTTPHeaderField: "X-Ios-Bundle-Identifier") // Build our API request let jsonRequest = [ "requests": [ "image": [ "content": imageBase64 ], "features": [ [ "type": "LABEL_DETECTION", "maxResults": 10 ], [ "type": "FACE_DETECTION", "maxResults": 10 ] ] ] ] // Serialize the JSON let jsonObject = JSON(jsonRequest) guard let data = try? jsonObject.rawData() else { return } request.httpBody = data // Run the request on a background thread DispatchQueue.global().async { self.runRequestOnBackgroundThread(request) } } func runRequestOnBackgroundThread(_ request: URLRequest) { // run the request let task: URLSessionDataTask = session.dataTask(with: request) { (data, response, error) in guard let data = data, error == nil else { print(error?.localizedDescription ?? "") return } self.analyzeResults(data) } task.resume() } } // FIXME: comparison operators with optionals were removed from the Swift Standard Libary. // Consider refactoring the code to use the non-optional operators. fileprivate func < <T : Comparable>(lhs: T?, rhs: T?) -> Bool { switch (lhs, rhs) { case let (l?, r?): return l < r case (nil, _?): return true default: return false } } // FIXME: comparison operators with optionals were removed from the Swift Standard Libary. // Consider refactoring the code to use the non-optional operators. fileprivate func > <T : Comparable>(lhs: T?, rhs: T?) -> Bool { switch (lhs, rhs) { case let (l?, r?): return l > r default: return rhs < lhs } }