- 投稿日:2019-03-01T20:53:51+09:00
ScrollViewとStackViewを使った簡単でレイアウト変更に強い実装方法について
はじめに
Storyboard上でのScrollViewの実装って結構ややこしいですよね。。。
Autolayoutの設定がわからないとかAutolayoutは設定できたけどスクロールがうまくいかないとか...
初心者の方は特に躓くのではないかと思います。そんなややこしいScrollViewの実装を簡単にできる方法を紹介したいと思います。
初心者以外の方に役立つと思うので是非ご覧ください。
みなさん、ScrollViewをStoryboardで実装するときどのように実装していますか?
ScrollViewのContentViewにUIViewを置いていませんか?ScrollViewの実装の仕方を調べると大抵の記事はcontentViewにUIViewを置いているのではないかと思います。
UIViewを置くことは間違いではないのですが、このUIViewをStackViewに置き換えることで簡単に実装することができレイアウトの変更に強くなります。ScrollViewのcontentViewにStackViewを配置する
ではさっそく実装していきましょう。
まずはScrollViewを配置してAutolayoutを設定します。
今回は上下左右0ptで指定します。
次にStackViewをScrollViewのcontentViewに配置します。
今回は縦方向のスクロールにするのでStackViewはVerticalのもを使います。
また、StackViewのDistributionをEqual Spacingにしましょう。※ここお忘れなく!AutolayoutはScrollViewのときと同じように上下左右0ptで設定します。
これだけだと赤いエラーが出るので加えてStackViewの横幅をScrollViewと同じにします。
ScrollViewのところまで引っ張って離すとWindowが出てくるのでEqual Widthsを選択します。
これでStackViewの横幅がScrollViewと同じになりました。ここまではUIViewでの実装と同じです。
UIViewでの実装との違い
ここまでのAutolayoutの設定でまだ赤いエラーが出てますね。
では配置したStackViewの中にUIViewを配置してみましょう。どうでしょう?赤いエラーが消えませんか?
ここでUIViewでの実装との違いを解説します。
contentViewがUIViewの場合、ここまでの手順で赤いエラーは消えません。
contentViewの高さが確定していないためです。
UIViewの場合、中に配置するUIパーツ全ての高さが確定し、全体の高さが決まっていないといけません。ですが、StackViewを使うと高さのAutolayoutを指定しなくてもエラーにならないのです。
全体のcontent sizeはStackViewが中のUIパーツの高さをみて勝手に計算してくれます。後は配置したUIViewの中で配置した各UIパーツにAutolayoutを指定していきましょう。
UIView自体に高さを指定しても大丈夫です。では実際に実行して見てみましょう。
まずは中のUIViewの高さを100にして見てみます。
この場合ScrollViewのcontent sizeが100になり、画面内に収まるのでスクロールできません。
次に高さを2000にして見てみましょう。
Storyboard上からはみ出ていますが、一旦気にしなくて大丈夫です。
気になる方はStoryboardのサイズを変えてください!少しわかりづらいかもしれませんが、スクロールができていると思います。
このようにcontent sizeはStackViewが勝手に計算してくれます。
StackViewを使うメリット
ScrollViewのcontent viewにStackViewを使うと以下のメリットがあります。
・全てのレイアウトが完成しなくてもエラーにならない
・content view(StackView)自体の高さのAutolayoutを設定しなくてよい
・レイアウトの修正をするときにAutolayoutの再設定が楽になる ※これが一番うれしいかも最後のメリットに関して加えて説明します。こちらは初心者以外の方にも良い情報かと思います。
中に配置するViewを切り出してxibで作る
先程配置したUIViewの中で全てのUIパーツを組み立てても良いのですが、デザインや実装の都合上区切りのよいところで分けてそれぞれをxibで作りましょう。
※分けたxibはそれぞれxib内でAutolayoutを設定してください。Storyboard上でデザインを確認したい方はxibのクラスに@IBDesignableを設定しましょう。xibのレイアウトが完成したらStackViewの中にUIViewを置いてUIViewクラスにxibで作ったクラスを指定します。
このとき配置したUIViewにはAutolayoutを指定しなくて大丈夫です。ここまでできたら実行してみてください。
どうでしょう?
Storyboard上ではAutolayoutを設定していませんが、ちゃんとxib側で設定したAutolayoutの通りの高さになっているのではないでしょうか?全てをStoryboard上で組み立てるとレイアウトの修正をするときAutolayoutの修正がとても大変です。
ですが、このようにxibに切り出して実装するとレイアウトの修正する際、切り出したxib内のAutolayoutを修正するだけでよくなります。以上でStackViewを使ったScrollViewの実装方法の解説を終わります。
是非お試しください!
- 投稿日:2019-03-01T20:53:51+09:00
ScrollViewとStackViewを使った簡単でレイアウト変更に強い実装方法
はじめに
Storyboard上でのScrollViewの実装って結構ややこしいですよね。。。
Autolayoutの設定がわからないとかAutolayoutは設定できたけどスクロールがうまくいかないとか...
初心者の方は特に躓くのではないかと思います。そんなややこしいScrollViewの実装を簡単にできる方法を紹介したいと思います。
みなさん、ScrollViewをStoryboardで実装するときどのように実装していますか?
ScrollViewのContentViewにUIViewを置いていませんか?ScrollViewの実装の仕方を調べると大抵の記事はcontentViewにUIViewを置いているのではないかと思います。
UIViewを置くことは間違いではないのですが、このUIViewをStackViewに置き換えることで簡単に実装することができレイアウトの変更に強くなります。ScrollViewのcontentViewにStackViewを配置する
まずはScrollViewを配置してAutolayoutを設定します。
今回は上下左右0ptで指定します。
次にStackViewをScrollViewのcontentViewに配置します。
今回は縦方向のスクロールにするのでStackViewはVerticalのもを使います。
また、StackViewのDistributionをEqual Spacingにしましょう。※ここお忘れなく!AutolayoutはScrollViewのときと同じように上下左右0ptで設定します。
これだけだとエラーが出るので加えてStackViewの横幅をScrollViewと同じにします。
ScrollViewのところまで引っ張って離すとWindowが出てくるのでEqual Widthsを選択します。
これでStackViewの横幅がScrollViewと同じになりました。ここまではUIViewでの実装と同じです。
UIViewでの実装との違い
ここまでのAutolayoutの設定でまだエラーが出てますね。
では配置したStackViewの中にUIViewを配置してみましょう。どうでしょう?エラーが消えませんか?
ここでUIViewでの実装との違いを解説します。
contentViewがUIViewの場合、ここまでの手順でエラーは消えません。
contentViewの高さが確定していないためです。
UIViewの場合、中に配置するUIパーツ全ての高さが確定し、全体の高さが決まっていないといけません。ですが、StackViewを使うと高さのAutolayoutを指定しなくてもエラーにならないのです。
全体のcontent sizeはStackViewが中のUIパーツの高さをみて勝手に計算してくれます。後は配置したUIViewの中で配置した各UIパーツにAutolayoutを指定していきましょう。
UIView自体に高さを指定しても大丈夫です。では実際に実行して見てみましょう。
まずは中のUIViewの高さを100にして見てみます。
この場合ScrollViewのcontent sizeが100になり、画面内に収まるのでスクロールできません。
次に高さを2000にして見てみましょう。
Storyboard上からはみ出ていますが、一旦気にしなくて大丈夫です。
気になる方はStoryboardのサイズを変えてください!少しわかりづらいかもしれませんが、スクロールができていると思います。
このようにcontent sizeはStackViewが勝手に計算してくれます。
StackViewを使うメリット
ScrollViewのcontent viewにStackViewを使うと以下のメリットがあります。
・全てのレイアウトが完成しなくてもエラーにならない
・content view(StackView)自体の高さのAutolayoutを設定しなくてよい
・レイアウトの修正をするときにAutolayoutの再設定が楽になる ※これが一番うれしいかも中に配置するViewを切り出してxibで作る
先程配置したUIViewの中で全てのUIパーツを組み立てても良いのですが、デザインや実装の都合上区切りのよいところで分けてそれぞれをxibで作りましょう。
※分けたxibはそれぞれxib内でAutolayoutを設定してください。Storyboard上でデザインを確認したい方はxibのクラスに@IBDesignableを設定しましょう。xibのレイアウトが完成したらStackViewの中にUIViewを置いてUIViewクラスにxibで作ったクラスを指定します。
このとき配置したUIViewにはAutolayoutを指定しなくて大丈夫です。ここまでできたら実行してみてください。
どうでしょう?
Storyboard上ではAutolayoutを設定していませんが、ちゃんとxib側で設定したAutolayoutの通りの高さになっているのではないでしょうか?全てをStoryboard上で組み立てるとレイアウトの修正をするときAutolayoutの修正がとても大変です。
ですが、このようにxibに切り出して実装するとレイアウトの修正する際、切り出したxib内のAutolayoutを修正するだけでよくなります。以上、StackViewを使ったScrollViewの実装方法でした。
- 投稿日:2019-03-01T16:55:56+09:00
R.swift をどうしても Carthage で使いたいあなたへ(改)
R.swift をどうしても Carthage で使いたいあなたへという記事があります。今 CocoaPods との戦いでとても疲れたので、CocoaPods を排除するために R.swift のインストールでとても参考になりました。
今回自分の場合は、理論上、R.swift はビルドタイムで勝手にリソースへのコードを生成してくれるツールであってライブラリーではない、ということは逆にいうとそもそも Carthage ではなく Homebrew で取り込められるはずだと思いました。実際 Homebrew 自身も rswift のインストールのための Formula も公開しているためそのまま
$ brew install rswiftを叩けば勝手に R.swift がインストールされます。ところがそれで自動生成されたファイルには結局
import Rswiftがあったのでビルドで躓いて、そこでググって出てきたのが最初に紹介した記事です。なるほどふむふむ、つまりは R.swift の利用にはどうやら R.swift.Library も必要で、そしてこれは Carthage からでも落とせる、ということですね。というわけで最終的には Homebrew と Carthage だけで R.swift の導入に成功しました(RswiftDownloader は必要ありませんでした)ので、簡単に手順をまとめます:
- Homebrew から rswift の自動生成プログラムをインストールします(Brewfile を利用している場合は
brew "rswift"の行を Brewfile に追加し、利用していない場合はそのまま$ brew install rswiftのコマンドをターミナルから叩けば OK です)- Carthage で R.swift.Library を導入します(Cartfile に
github "mac-cain13/R.swift.Library"の行を追加し、あとは他の Carthage で導入するライブラリーと同じような設定方法で設定します)- 公式のインストール手順の Manually の 2 番から続けます(ただし
rswiftの呼び出しはそもそも Homebrew によって導入済みなので、"$SRCROOT/rswift"のように呼び出さなくても普通にrswiftで呼び出せます)以上で、R.swift を CocoaPods の代わりに Homebrew と Carthage で簡単に導入できます。
- 投稿日:2019-03-01T16:41:36+09:00
CALayerでユーザーの目を引くUIを作る
概要
CALayerを用いたユーザーに一点を注目させるUI
実装環境
Xcode 10.1
Swift 4.2.1サンプル
maskボタンをタップするとmaskが現れおじさんの顔をタップするとmaskが解除される。
コード
import UIKit class ViewController: UIViewController { ///masked layer let target = CALayer() ///mask layer let shapeLayer = CAShapeLayer() ///check masked var isMaskedNow = false override func viewDidLoad() { super.viewDidLoad() createImage() createButton() } func createImage() { let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) imageView.center = self.view.center imageView.image = UIImage(named: "image1") imageView.contentMode = .scaleAspectFill self.view.addSubview(imageView) } func createButton() { let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50)) button.center = CGPoint(x: self.view.center.x, y: self.view.center.y + 250) button.addTarget(self, action: #selector(onTapped), for: .touchUpInside) button.backgroundColor = .gray button.setTitle("mask", for: .normal) self.view.addSubview(button) } @objc func onTapped(){ if isMaskedNow{return} isMaskedNow = true appearAnimation(appearRect: CGRect(x: 265, y: 245, width: 100, height: 100), duration: 1.0) } ///appear layer animation func appearAnimation(appearRect: CGRect, duration:Double){ target.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height) target.backgroundColor = UIColor.black.cgColor shapeLayer.fillRule = .evenOdd target.mask = shapeLayer self.view.layer.addSublayer(target) //light animation begin path var begin = appearRect begin.size = CGSize(width: 0, height: 0) let path = UIBezierPath(ovalIn: begin) path.append(UIBezierPath(rect: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))) let beginPath = path.cgPath //light animation end path var end = appearRect end.origin = CGPoint(x:end.origin.x - end.size.width/2, y:end.origin.y - end.size.height/2) let path2 = UIBezierPath(ovalIn: end) path2.append(UIBezierPath(rect: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))) let endPath = path2.cgPath ///expand light animation let lightAnimation = CABasicAnimation(keyPath: "path") lightAnimation.fromValue = beginPath lightAnimation.toValue = endPath ///fade in animation let backgroundOpacityAnimation = CABasicAnimation(keyPath: "opacity") backgroundOpacityAnimation.fromValue = 0 backgroundOpacityAnimation.toValue = 0.6 let animationGroup = CAAnimationGroup() animationGroup.duration = duration animationGroup.repeatCount = 0 animationGroup.animations = [backgroundOpacityAnimation, lightAnimation] animationGroup.fillMode = .forwards animationGroup.isRemovedOnCompletion = false shapeLayer.add(animationGroup, forKey: nil) } ///fade out layer func fadeOutAnimation(duration: Double) { let fadeOut = CABasicAnimation(keyPath: "opacity") fadeOut.toValue = 0 fadeOut.duration = duration fadeOut.fillMode = .forwards fadeOut.isRemovedOnCompletion = false shapeLayer.add(fadeOut, forKey: nil) } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else {return} let point = touch.location(in: self.view) //if point is inside of the circle, fade out mask and make isMaskedNow true if isMaskedNow{ if !(CGRect(x: 265 - 100/2, y: 245 - 100/2, width: 100, height: 100).contains(point)) { return } fadeOutAnimation(duration: 0.3) isMaskedNow = false } } }解説
shapeLayer.fillRule = .evenOdd
.evenOdd
レイヤーのどこに色を塗るかを判定している。以下、公式ドキュメントより
If the number of crossings is even, the point is outside the path. If the number of crossings is odd, the point is inside the path and the region containing it should be filled.
つまりpathの数が奇数か偶数で塗り分けられる。
今回の場合pathは二つなので円の外側が塗られる。target.mask = shapeLayer
.mask
設定するレイヤーの形にターゲットを切り取る。
.fromValue, .endValue
それぞれアニメーションの前後の状態を設定する。
animationGroup.fillMode = .forwards
.forwards
アニメーション後のレイヤーの状態を残す。
デフォルトでは消えるようになっている。animationGroup.isRemovedOnCompletion = false
アニメーション後にレイヤーからアニメーションを削除するかどうか。
CABasicAnimation(keyPath: "path")
keyPathでアニメーションの種類を設定する。
keyの種類については以下を参照。
CALayer Animatable Properties参考
チュートリアルなどで使えるスポットライトっぽいUIを作ってみました
UIViewをくり抜くまとめ
参考のものを使って動かそうとしたが数年前のもののため上手く動かなかったため今回実装した。
ソースはこちら↓で管理
strawhare/mask_layer
- 投稿日:2019-03-01T16:29:48+09:00
UIPickerView(ドラムロール)の値が?(クエスチョン)になってしまって泥沼だった話
ドラムロールを作ってくれと言われたので……
早速作ったわけですが、苦戦しました。
なんでかって、この1、2、3と縦に並んでる部分があるじゃないですか。
これがなんと、全てハテナマークで表示されてしまうんです(この写真では直っています)色んなところでドラムロールのコードは掲載されていますがどれも同じ現象に悩まされました。
原因を調べてもなぜか数えるほどしか出てこない。。。(しかも解決していない)これは記録しておこうということで。
まず、最初に調べたコードです。
ドラムロールを作る際にほぼ最低限必要な項目です。import UIKit // UIPickerViewDastaSourceとUIPickerViewDelegateは継承しなければエラーが出ます(fixをクリックすれば直してくれますが) class ViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate { @IBOutlet weak var pickerTextField: UITextField! var array:[String] = ["1", "2", "3"] var pickerView = UIPickerView() override func viewDidLoad() { super.viewDidLoad() // pickerViewの位置と大きさを指定 pickerView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: pickerView.bounds.size.height) // データソースを設定(よくわからない) pickerView.dataSource = self // デリゲートを設定(これを書くのを怠るとドラムロールにハテナすら表示されません。空っぽです) pickerView.delegate = self // 初期に選択されているドラムロールの値(必ずしも必要ではない) pickerView.selectRow(0, inComponent: 0, animated: true) // ここでいうtextViewはドラムロールで選択した値が収納されるテキストボックスです(画像でいう、2と書かれた場所) self.textView.inputView = pickerView } // ドラムロールの数です。今回は一つなので1を返します func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } // ドラムロールに表示する値の数を返します func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return array.count } // ドラムロールに表示する値(文字列)をここで返します func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return array[row] } // ドラムロールにて選択した値をtextViewに設定します func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { textView.text = array[row] }このコードはなぜか有名で、これに一味加えたくらいのソースコードはわんさか出てきました。
しかしどれを使ってもいくつか警告やエラーが出てきて、クリックして直してくとドラムロールの値はハテナとなりました。ちなみにこれをfixをクリックして直していくと
// ドラムロールに表示する値(文字列)をここで返します fileprivate func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return array[row] }↑fileprivateがつけられてしまいます。
どうやらこれが原因だったようで。。。
多分、fileprivateがつけられてしまう理由は、pickerViewという関数が三つあって、内二つが引数の数と型が一致しているからかと思います。
関数の区別がつかないからとりあえずfileprivateを付けろ的なw
そのせいで外から呼び出せなかったのだと思います。
値が取れないのでブレイクポイントを張ったらそこを通らず、初めて気づきましたwとりあえずfileprivateを外して引数の型など諸々をいじったところ、最終的には
// ドラムロールに表示する値(文字列)をここで返します func pickerView(_ pickerView: UIPickerView, viewForRow row: Int,forComponent component: Int, reusing view: UIView?) -> UIView{ let label = UILabel() label.text = array[row] label.textAlignment = NSTextAlignment.center return label }これに書き換えたところうまく表示されました。
さっきまでStringで返してたのにUIViewで返して大丈夫?と心配だったのですが問題ありませんでした。すごく初歩的なのでしょうけど、 pickerView自体ブラックボックスで仕組みがイメージできていないためこのような初歩的な部分に気づかなかったのだと思います。
精進しますw
- 投稿日:2019-03-01T15:53:54+09:00
iOSでスクショ、画面録画時にトリガーする処理を実装する
具体的にどういうことをしたいかというと、NetflixやAmazon Primeビデオなど動画配信サービスアプリ内でスクショをすると著作権、データ保護(DRM)のため、黒塗りされるのです。
(ちなみに画面録画すると、録画終了時に動画が保存されませんでしたとエラーが出ます)
Netflix Amazon Prime ビデオ UI周りのボタンはそのままですが、肝心の動画部分は黒塗りされています。
これ、実装してみたい!って思うじゃないですか。ということで、調べてみると、結論から言うと、iOS12リリース時点では不可能なんですね。
なぜなら、スクショを撮った瞬間、画面録画がスタートされる瞬間を検知する仕組みがないからです。
ですが、スクショを撮った後、画面録画を終了する時点は検知する仕組みはあったので、紹介したいと思います。
スクリーンショットを撮った後を検知する
使うのは
userDidTakeScreenshotNotificationです。○userDidTakeScreenshotNotification - UIApplication | Apple Developer Documentation
https://developer.apple.com/documentation/uikit/uiapplication/1622966-userdidtakescreenshotnotificatio
NotificationCenterの通知を使って検知します。
これは何を検知するとかいうとUIScreen.main.isCapturedの値が変わった時に呼ばれます。
userDidTakeScreenshotNotificationはスクリーンショットを撮った時の通知です。override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.addObserver( self, selector: #selector(didTakeScreenCaptured(_:)), name: UIApplication.userDidTakeScreenshotNotification, object: nil ) } @objc private func didTakeScreenCaptured(_ notification: Notification) { blackView.isHidden = UIScreen.main.isCaptured }上記はスクリーンショットを撮影後、隠しているビューを表示するというものです。
実際に撮影してみると、スクリーンショットはそのまま撮影できますが、その直後にメソッドが呼ばれて処理されます。
画面収録した後を検知する
使うのは
capturedDidChangeNotification○capturedDidChangeNotification - UIScreen | Apple Developer Documentation
https://developer.apple.com/documentation/uikit/uiscreen/2921652-captureddidchangenotification使い方は
userDidTakeScreenshotNotificationと同じで、viewDidLoad(_:)などで実装します。override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.addObserver( self, selector: #selector(didCapturedScreen(_:)), name: UIScreen.capturedDidChangeNotification, object: nil ) } @objc private func didCapturedScreen(_ notification: Notification) { yellowView.isHidden = UIScreen.main.isCaptured }こちらも
UIScreen.main.isCapturedの値が変更になったら、呼ばれます。簡単ですが、サンプルを作ってみました。
こちらからどうぞ。○ScreenCapturedSample
https://github.com/takashings/ScreenCapturedSample実際にどういう時に使うのか
正直
userDidTakeScreenshotNotificationとuserDidTakeScreenshotNotificationでは撮影後に呼ばれるため、何の意味もないのでは?という風に思いますよね。
はい、僕も同じく同様の感想です。ですが、こういう仕組みがあるからには何か活用できるのではないかと思います。
考え得る使いどころとしては、スクショ、画面録画をしたら個人で楽しむ分には良いけど、ネットに流さないでねとアラートを表示したり、逆に今スクショしたものをSNSにシェアしませんか?と促す(特にゲームでは使えるかも?)という感じでしょうか。
将来的にはもしかしたら、
userWillTakeScreenshotNotificationやuserWillTakeScreenshotNotificationのようなものが実装されるかもしれませんね(されるといいな)。







