以前書いたカスタムビューの実装方法の記事一覧メモ
Huawei 端末で Android アプリのログが Logcat に表示されない件
環境:
Android Studio 3.0.1
Android アプリのデバッグログがシミュレータ操作時には Logcat コンソールに表示されるが、Huawei P9 lite
端末を接続して操作した場合にはなぜか出力されない。
調べた結果、Huawei 端末側のログ出力設定を変更することで表示されるようになった。
その方法だが
- 電話アプリを起動し、ダイヤル画面から以下を入力する
*#*#2846579#*#*
- 隠しメニューが起動するので
Project Menu > Background Settings > LOG Settings
から表示したいログを選択
これで表示されるようになった。
Swift で NSAttributedString 内の部分文字列を別の文字列に置換する方法
環境: Swift3
StroyBoard 上で UILabel の文字列を Plain ではなく Attributed で設定し、その文字列内の PlaceHolder を後から別の文字列に置き換えたいケースがあり必要になった。
NSMutableAttributedString#replaceCharacters(in:with:)
メソッドを使用する。
まず、NSAttributedString の Extension としてメソッドを追加する。
extension NSAttributedString { func replace(pattern: String, replacement: String) -> NSMutableAttributedString { let mutableAttributedString = self.mutableCopy() as! NSMutableAttributedString let mutableString = mutableAttributedString.mutableString while mutableString.contains(pattern) { let range = mutableString.range(of: pattern) mutableAttributedString.replaceCharacters(in: range, with: replacement) } return mutableAttributedString } }
そして、以下のように呼び出して使う。
@IBOutlet weak var detailLabel: UILabel? override func viewDidLoad() { super.viewDidLoad() if let attributedText = self.detailLabel?.attributedText { self.detailLabel?.attributedText = attributedText.replace(pattern: "置き換えたい文字列", replacement: "新しい文字列") } }
UITextView を使ってテキストの一部をハイパーリンク化する
環境: Swift3
iOS10
UILabel ではなく UITextView の attributedText に NSLinkAttributeName をセットすることで簡単にできた。
クリック時にデフォルトでは Safari が起動して設定したURLのページが表示された。
クリック時の挙動を変更したい場合は UITextViewDelegate の
func textView(UITextView, shouldInteractWith: URL, in: NSRange, interaction: UITextItemInteraction)
(iOS 10から)
または
func textView(UITextView, shouldInteractWith: URL, in: NSRange)
(iOS7,8,9はこちら。iOS 10でDeprecated)
を実装して制御する。
今回は SFSafariViewController で開いてみた。
以下が実装例。
import UIKit import SafariServices class ViewController: UIViewController, UITextViewDelegate { @IBOutlet weak var textView: UITextView! override func viewDidLoad() { super.viewDidLoad() setupTextView() } func setupTextView() { let text = "詳細はこちらをご覧ください" textView.delegate = self textView.isSelectable = true textView.isEditable = false textView.textContainer.lineFragmentPadding = 0 textView.textContainerInset = .zero textView.isScrollEnabled = false // これが必要な理由は後述 let attributedString = NSMutableAttributedString(string: text) let range = NSString(string: text).range(of: "こちら") attributedString.addAttribute( NSLinkAttributeName, value: "https://www.google.co.jp/", range: range) textView.attributedText = attributedString textView.linkTextAttributes = [NSForegroundColorAttributeName: UIColor.red] } // MARK: - UITextViewDelegate // この Delegate の実装しない場合はデフォルトで URL を Safari で開く。 func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { // UIApplication.shared.open(URL) let controller = SFSafariViewController(url: URL) self.present(controller, animated: true) return false } }
これでテキストの一部をハイパーリンク化できる。
UITextView のレイアウト制約について
ここからはハイパーリンク化とは関係のない UITextView の挙動でちょっとハマった話。
上記例での UITextView のレイアウトは Storyboard の AutoLayout を使って行っていたのだが、UITextView の高さの制約を指定しないと制約エラーになった。
デバイスによって横幅が変わるので、高さの制約はテキストの長さに応じて自動的に設定して欲しいところ。
UILabel であればテキストが長くて複数行に渡る場合でも intrinsicContentSize が自動的に設定されるので AutoLayout で高さの制約は指定しなくてもよい。
UITextView も同じような挙動を期待していたが、 intrinsicContentSize を見てみたところ、-1(UIViewNoIntrinsicMetric)でサイズ不定となっていたため、高さの制約指定が必要であった。
ただし、これは UITextView の isScrollEnabled プロパティを false とすること(デフォルトはtrue)で intrinsicContentSize が自動設定され、高さの制約指定は不要になることがわかった。
つまり、UITextView は UIScrollView のサブクラスであるため、isScrollEnabled=true
の場合は、枠となる矩形のサイズと、内部のスクロールさせる矩形のサイズは違うため、枠となる矩形側の高さの制約指定は必要となり、isScrollEnabled=false
にすればスクロール不要となるので UILabel と同様に高さが自動的に設定されるのだと思われる。
UITextView の下側のテキストが切れてしまう件
これも余談だが、UITextView に複数行に渡る長いテキストが設定されている場合に下側のテキストが切れてすべて表示されないことがあった。
これはデフォルトのシステムフォントを使っている場合には起きないのだが、ヒラギノフォントを使った場合に発生した。
解決法としては
textView.isScrollEnabled = false textView.isScrollEnabled = true
というように一旦、isScrollEnabled = false
すればよいらしい。
たしかにこれで解消された。
追記
Swift 5バージョン
xyk.hatenablog.com
URL Scheme の追加と Configuration によって変更する方法
環境:
Xcode8.3.2
Swift3
URL Scheme の追加
Target -> Info -> URL Types から URL Scheme を追加する。
まだ何も追加していない状態。
+ボタンを押してidentifier
とURL Schemes
を追加した。
URL Schemes にはカンマ区切りで複数入力も可能。
identifier: com.example.myapp URL Schemes: myapp
Info.plist に URL Types の項目が追加された。
Info.plist のソースコードを直接見ると以下のようになっている。
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLName</key> <string>com.example.myapp</string> <key>CFBundleURLSchemes</key> <array> <string>myapp</string> </array> </dict> </array>
Configuration によって URL Scheme を変更する
Release 環境では上で設定したmyapp
でよいが、Debug 環境ではmyapp-debug
に変更したい。
そこで Run Script を使ってコンパイル時に自動で更新させる。
シェルコマンドは Run Script の入力欄に直接を書いてもよいのだがproject.pbxproj
に埋め込まれてしまうので別ファイルに切り出すことにする。
以下のようにScirptsディレクトリを作成し、そこにシェルスクリプトファイルを置いた。
シェルスクリプトの中身。CONFIGURATION
がDebug
のときのみ書き換えるようになっている。
URL Schemes は複数設定できるが今回は1つ目に設定されたものを更新対象にしているので注意。
#!/bin/sh if [ ${CONFIGURATION} = "Debug" ]; then lower_conf=`echo ${CONFIGURATION} | tr 'A-Z' 'a-z'` myapp_url_scheme="myapp-$lower_conf" info_plist_file_path="${TARGET_BUILD_DIR}/${PRODUCT_NAME}.app/Info.plist" /usr/libexec/PlistBuddy -x -c "Set :CFBundleURLTypes:0:CFBundleURLSchemes:0 $myapp_url_scheme" "$info_plist_file_path" updated_myapp_url_scheme=`/usr/libexec/PlistBuddy -c "Print CFBundleURLTypes:0:CFBundleURLSchemes:0" "$info_plist_file_path"` echo "MyApp URL Scheme set to: $updated_myapp_url_scheme" fi
Build Phases
から新たな Run Script を追加する。
追加する箇所はcompile sources
とcopy bundle resources
フェーズの終わった後。
Run Script
のところをクリックすると名前が変更できる。ここでは以下のように変更した。
先ほど作成したシェルスクリプトを実行させる。
. ${PROJECT_DIR}/Scripts/url_schemes.sh
そしてビルドを実行する。
変更されているか Info.plist を確認。
${TARGET_BUILD_DIR} 以下の Info.plist のパス
~/Library/Developer/Xcode/DerivedData/MySample-gbqatlezfkhslgeiregazsgqloel/Build/Products/Debug-iphonesimulator/MySample.app/Info.plist
myapp-debug
に書き換わっている。
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLName</key> <string>com.example.myapp</string> <key>CFBundleURLSchemes</key> <array> <string>myapp-debug</string> </array> </dict> </array>
Run Script の実行結果はCmd + 8
開く Report Navigator に表示されるのだが、echoコマンドで出力したログはAll Issues
には出ないのでAll Messages
の方で確認する。
ちなみに
echo "error: xxx" echo "warning: xxx"
という書き方をするとAll Issues
の方にもログが出力される。
Fastlane で URL Scheme を更新するやり方は以前書いた。
View の Auto Layout によるアニメーションを無効にする
環境: Swift3
あるViewのSubviewのレイアウトをAuto Layoutで行った時にアニメーションしながら配置された。
この時のアニメーションは不要なので無効にする。
やり方はSubviewの配置が行われるlayoutSubviews
メソッドをオーバーライドして以下のようにアニメーションしないようにする。
方法1
UIView クラスメソッドのperformWithoutAnimation:
のブロック内で実行させる。
override func layoutSubviews() { UIView.performWithoutAnimation { super.layoutSubviews() } }
方法2
CATransaction.setDisableActions(true)
を実行後にlayoutSubviews
を実行させる。
override func layoutSubviews() { CATransaction.begin() CATransaction.setDisableActions(true) super.layoutSubviews() CATransaction.commit() }
角丸なUIViewに角丸な影をつける
環境: Swift3
角丸なUIViewに角丸なドロップシャドウをつけるやり方。
shadowView.layer.cornerRadius = 10 shadowView.layer.masksToBounds = false shadowView.layer.shadowColor = UIColor.black.cgColor shadowView.layer.shadowOffset = CGSize(width: 5, height: 5) // 影の方向(ここでは右下) shadowView.layer.shadowOpacity = 0.5 // 影の濃さ shadowView.layer.shadowRadius = 5 // 影のぼかし量
プレイグラウンドで確認。
import UIKit import PlaygroundSupport let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200)) view.backgroundColor = UIColor(red: 255/255, green: 110/255, blue: 134/255, alpha: 1) let shadowView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) shadowView.backgroundColor = UIColor(red: 89/255, green: 172/255, blue: 255/255, alpha: 1) shadowView.center = view.center shadowView.layer.cornerRadius = 10 shadowView.layer.masksToBounds = false shadowView.layer.shadowColor = UIColor.black.cgColor shadowView.layer.shadowOffset = CGSize(width: 5, height: 5) shadowView.layer.shadowOpacity = 0.5 shadowView.layer.shadowRadius = 5 // 以下、角丸パス追加とラスタライズで高速化 shadowView.layer.shadowPath = UIBezierPath(roundedRect: shadowView.bounds, cornerRadius: 10).cgPath shadowView.layer.shouldRasterize = true shadowView.layer.rasterizationScale = UIScreen.main.scale view.addSubview(shadowView) PlaygroundPage.current.liveView = view
例2:
ぼんやりしてない影をちょっとだけをつけたい場合
shadowRadius を小さくするすると以下のようなかんじになった。
import UIKit import PlaygroundSupport let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200)) view.backgroundColor = .groupTableViewBackground let shadowView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) shadowView.backgroundColor = .white shadowView.center = view.center shadowView.layer.cornerRadius = 10 shadowView.layer.masksToBounds = false shadowView.layer.shadowColor = UIColor.black.cgColor shadowView.layer.shadowOffset = CGSize(width: 2, height: 2) shadowView.layer.shadowOpacity = 0.1 shadowView.layer.shadowRadius = 0 view.addSubview(shadowView) PlaygroundPage.current.liveView = view
StackOverFlowにわかりやすいサンプル例があったので転載。
CGPath の変化をアニメーションさせるサンプル
環境: Xcode8.2.1, Swift3
CGPath の変化をアニメーションさせる方法を試した。
気をつける点としては変更前と変更後のパスの数を同じにしておくこと。
import UIKit import PlaygroundSupport class SquareButton: UIControl { let pathLayer = CAShapeLayer() var squarePath: UIBezierPath! var halfMoonPath: UIBezierPath! required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } override init(frame: CGRect) { super.init(frame: frame) self.squarePath = self.squarePath(rect: frame) self.halfMoonPath = self.halfMoonPath(rect: frame) self.pathLayer.fillColor = UIColor.orange.cgColor self.pathLayer.strokeColor = UIColor.white.cgColor self.pathLayer.lineWidth = 4 self.pathLayer.path = self.squarePath.cgPath self.layer.addSublayer(self.pathLayer) } override func endTracking(_ touch: UITouch?, with event: UIEvent?) { super.endTracking(touch, with: event) self.toggle() } func toggle() { let anim = CABasicAnimation(keyPath: "path") if self.isSelected { anim.fromValue = self.halfMoonPath.cgPath anim.toValue = self.squarePath.cgPath } else { anim.fromValue = self.squarePath.cgPath anim.toValue = self.halfMoonPath.cgPath } anim.duration = 0.4 anim.fillMode = kCAFillModeForwards anim.isRemovedOnCompletion = false self.pathLayer.add(anim, forKey: "animatePath") self.isSelected = !self.isSelected } func squarePath(rect: CGRect) -> UIBezierPath { let initialPoint = CGPoint(x: 0, y: 0) let curveStart = CGPoint(x: rect.maxX * 0.05, y: 0) let curveControl = CGPoint(x: rect.maxX * 0.5, y: 0) let curveEnd = CGPoint(x: rect.maxX * 0.95, y: 0) let firstCorner = CGPoint(x: rect.maxX, y: 0) let secondCorner = CGPoint(x: rect.maxX, y: rect.maxY) let thirdCorner = CGPoint(x: 0, y: rect.maxY) let myBezier = UIBezierPath() myBezier.move(to: initialPoint) myBezier.addLine(to: curveStart) myBezier.addQuadCurve(to: curveEnd, controlPoint: curveControl) myBezier.addLine(to: firstCorner) myBezier.addLine(to: secondCorner) myBezier.addLine(to: thirdCorner) myBezier.close() return myBezier } func halfMoonPath(rect: CGRect) -> UIBezierPath { let initialPoint = CGPoint(x: 0, y: 0) let curveStart = CGPoint(x: rect.maxX * 0.05, y: 0) let curveControl = CGPoint(x: rect.maxX * 0.5, y: rect.maxY * 0.6) let curveEnd = CGPoint(x: rect.maxX * 0.95, y: 0) let firstCorner = CGPoint(x: rect.maxX, y: 0) let secondCorner = CGPoint(x: rect.maxX, y: rect.maxY) let thirdCorner = CGPoint(x: 0, y: rect.maxY) let myBezier = UIBezierPath() myBezier.move(to: initialPoint) myBezier.addLine(to: curveStart) myBezier.addQuadCurve(to: curveEnd, controlPoint: curveControl) myBezier.addLine(to: firstCorner) myBezier.addLine(to: secondCorner) myBezier.addLine(to: thirdCorner) myBezier.close() return myBezier } } let baseView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 200)) baseView.backgroundColor = UIColor(red: 38.0/255, green: 151.0/255, blue: 68.0/255, alpha: 1) let button = SquareButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) baseView.addSubview(button) button.center = baseView.center PlaygroundPage.current.liveView = baseView
UIScrollView で現在のページ数を取得する
環境: Xcode8.2.1, Swift3
UIScrollViewのisPagingEnabled
プロパティを true にすると、ページ単位のスクロールが可能になる。
このときに現在のページ数を求める方法についてメモ。
ページングは横スクロールの場合なら UIScrollView のcontentOffset.x
がUIScrollView.bounds.width
の半分を超えたところでドラッグを離すと隣のページに進み、半分を超えてなければ元のページに戻る挙動になっている。
まず、その時点のcontentOffset.x
からページ数を計算する Extension を追加する。
extension UIScrollView { var currentPage: Int { return Int((self.contentOffset.x + (0.5 * self.bounds.width)) / self.bounds.width) + 1 } }
で、今回はページングのスクロールが完全に止まったタイミングでページ数を取得する方法を考える。
スクロールが完全に止まったタイミングを検出するには前回調べた UIScrollViewDelegate のメソッドに仕掛ければよい。
基本scrollViewDidEndDecelerating
のみで良いと思う(isPagingEnabled=true
はページの区切りまで自動スクロールするので)が、この Delegate はドラッグをピタッと止めた場合は呼ばれないので、その時でも検出できるように念のためscrollViewDidEndDragging
でかつdecelerate=false
の場合にも取得するようにしておく。
// MARK: - UIScrollViewDelegate func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { print("currentPage:", scrollView.currentPage) } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { print("currentPage:", scrollView.currentPage) }
これでページ数が切り替わった(ページングのスクロールが止まった)時に1度のみページ数が表示される。