- 投稿日:2020-02-05T23:13:57+09:00
#23 Viewの背景を画像にする方法
はじめに
個人のメモ程度の出来なのであまり参考にしないで下さい.
環境
Xcode:11.2.1
Swift:5.1.2
2019/11part1
Assets.cassests
に画像をドロップする.part2
UIImageView
と検索する.part3
UIImageView
をView
に全て覆うように置く.part4
UIImageView
に以下の4つの制約を付ける.part5
すると
UIImageView
がズレるので,もう一度View
に全て覆うようにする.part6
その状態で,
Selected Views
のUpdate Constraint Constants
を選択する.part7
そのまま
UIImageView
を選択した状態で,Attributes inspector
のImage
から,先程追加した画像を選択する.part8
UIImageView
を選択した状態で,Attributes inspector
のContent Mode
でScale To Fill
を選択する.part9
ラベルやボタンよりも後に
UIImageView
を配置すると,ラベルやボタンが隠れてしまうので,下の写真のように,UIImageView
をSafe Area
の直下にドラッグ・ドロップする.part10
以下のように,
UIImageView
が再背面に移動している事を確認する.part11
Label Label
を選択して,Attributes inspector
のShadow
をSystem Background Color
にする.part12
Label Label
を選択した状態で,Attributes inspector
のShadow Offset
のWidth
とHeight
を-1
にする.part13
Button!
を選択して,Attributes inspector
のFont
のStyle
をBold
にする.
- 投稿日:2020-02-05T20:07:40+09:00
iOSエンジニアがSign in with Appleをサーバーサイドで実装したときに行き詰りを感じたところとか
Sign in with Apple 実装のデッドラインは4月 ですね☺️
Web/iOS/Androidなど、複数プラットフォームをサポートするためWebViewでログイン機能を実装しているサービスや、外部サービスAPI利用の際に独自のAPIを噛ませているなど、サーバーサイドでSign in with Appleに対応するケースはあるかと思います。iOSエンジニアであるわたしがサーバーサイドで実装した際に、わかりにくいな〜?と感じたポイントについて記載します。
自分のスキルセットとして、認証・認可、Webの知識はほぼ0でした?
実装したソースコードをの抜粋を載せていますが、言語はrubyです。やること
今回ご説明する Sign in with Apple の全体のざっくりフローは、
- Apple Developer サイトでの設定
- Apple ID サインイン画面表示(Authorizeエンドポイント)、認可コード受取り
- 認可コードを使い、AppleのユーザーIDを取得(Tokenエンドポイント)
です。
取得したいデータがemailとnameのみの場合 → ステップ2まで
ユーザーIDを取得したい場合 → ステップ3まで
実装する必要があります。参考までに前知識
Auth関連
まず最初に、OAuth周りの知識があると理解が早いと思います。
この方のqiitaが非常に参考になりました。(ありがとうございました。)Appleガイドライン関連
Apple Developer サイトでのもろもろ
ではさっそくApple Developerサイトを開きます。
Servise ID作成からドメイン認証
- 対象の App ID を作成し、Capabilities として Sign in with Apple を追加する
- Apple Developer の Certificates, Identifiers & Profiles から Service ID を追加する
- Primary App ID を 1 のAppに指定する
- Domainsに認可コードのリダイレクト先のドメインと Return URLs を指定する
- Domainの検証ファイル
apple-developer-domain-association.txt
をダウンロードする- Domainの検証ファイルをWebサイトの
https://.../.well-known/
ディレクトリに置くService ID と Return URLs はAuthorize、Tokenエンドポイントで利用します。
ドメイン検証に失敗したら
https://{yourdomain}/.well-known/apple-developer-domain-association.txt
にアクセスできるか- 検証ファイルは有効期限内か(7日間のみ有効)
- 検証ファイルのダウンロードは何回でも可能なので、常に最新のファイルを使う
- サーバーがTLSの条件を満たしているか
を確認する。
明文化されていなさそうですが、Appleさんからサポートを受けた開発者がフォーラムに書き込んでいます。
https://forums.developer.apple.com/thread/122124そのほか、ネットワーク構成などに問題がないか等、公式の Troubleshooting Domain Verification を読んでみるとよいです。
Key をダウンロード
Tokenエンドポイントのリクエストパラメータの JSON Web Token 作成に使います。
当然ですが、センシティブなファイルなので暗号化などよしなに行い、サーバーのどこか安全なところに置いておく。
今回はわたしは、.txt
に変換して使いました。公式ドキュメントは Create a Sign in with Apple private key
Apple IDサインインページ表示
Appleサインインボタン
公式ドキュメント Incorporating Sign in with Apple into Other Platforms の Add a Custom “Sign in with Apple” Button にダウンロードURLやサイズ、カラー指定のクエリパラメータの説明があります。
Human Interface Guidelines にしたがって正しくつかいましょう。Authorizeエンドポイント
Appleの認証ページURLのクエリパラメータ、レスポンスについては Incorporating Sign in with Apple into Other Platforms に記載があります。
大事なのは、
client_id = Service IDで指定した文字列 redirect_uri = Service IDに設定したリダイレクトURIです。
response_type
=id_token
にする場合、response_mode
はfragment
またはform_post
である必要がある- 氏名やメールアドレスを取得したい(
scope
を指定する)場合、response_mode
=form_post
にする必要があるなど、諸々の制約があります。
サインインページが表示されると、ユーザーによるApple IDの認証を受け付けます。
認証が完了すると、指定したリダイレクトURIにリダイレクトが行われます。
受け取れる値は認可コードと、scope
に指定がある場合初回1度のみユーザーID以外の個人情報(メールアドレス、氏名など)が受け取れます。サインインページがうまく表示されない
invalid_uri や invalid_client が表示されたら、焦らず
client_id
とredirect_uri
を今一度確認しましょう。
開発・本番など環境別でドメインを分けている場合、redirect_uri
が変わると思いますので要チェックです。
Apple Developer 上で作成した Service ID の Returns URLs に該当のリダイレクト先が登録されているか確認しましょう。アクセストークン・ユーザーID取得
Tokenエンドポイント
Appleのトークン発行エンドポイントにリクエストを行い、トークンの発行を行います。
パラメータ、レスポンスについては Generate and validate tokens に記載があります。
パラメータは Authorization のものを使います。
client_secret
の値のJSON Web Token(JWT)作成Apple Developers で発行したKeyを用いて、リクエストパラメータ
client_secret
に付与するJWTの作成を行います。
JWTとは、header
とpayload
から成るJSON構造をエンコードし電子署名をつけたものです。
デコード&エンコードしたJWTの検証は、 https://jwt.io/ がおすすめです。rubyでのJWTのエンコード&署名の例は以下です。
jwt/ruby-jwt というGemを利用しています。
kid
やiss
、sub
などは暗号化しておくと良いでしょう。require 'jwt' require 'openssl' # 署名用の鍵を取得 def pem file = "path/to/key.txt" # Apple Developerで発行したKeyファイル # key.txtを読み出し、暗号鍵を生成する OpenSSL::PKey::EC.new(File.read(file)) end # 署名付きIDトークンを取得 def id_token header = { 'kid' => 'Apple Developerで発行したKey ID' } claim = { 'iss' => 'Apple DeveloperでのTeam ID', 'iat' => Time.now.to_i, 'exp' => Time.now.to_i + 15777000, 'aud' => 'https://appleid.apple.com', 'sub' => 'Apple Developerで発行したService ID' } JWT.encode( claim, pem, 'ES256', header ) endレスポンスの検証
レスポンスに含まれるIDトークンの検証には、Appleの公開鍵取得が必要です。
エンドポイントについては Fetch Apple's public key for verifying token signature に記載があります。jwt/ruby-jwt を用いた検証の実装例です。
# IDトークンをデコードし署名を検証する # public_keysに渡すのは、https://appleid.apple.com/auth/keys の "keys"の値です。 def decode(id_token, public_keys) JWT.decode( id_token, nil, true, { algorithms: 'E256', jwks: { keys: public_keys } } ) endIDなどのユーザー情報
sub
の値がユーザーの一意の識別子(ユーザーID)です。
そのほかのレスポンスのIDトークンに含まれる情報については Retrieve the User’s Information from Apple ID Servers
に記載があります。
あとは取得したデータをもとに自サービスでのユーザー認証を完了させましょう。おしまい
書くと簡単ですが実際はたいへんだったのでiOS boys and girlsの皆様はできる限りネイティブでやりましょう
Thanks to
https://qiita.com/kiwi26/items/5b8cc53ed8d10a403f00
https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple以上です???
ありがとうございました?
- 投稿日:2020-02-05T19:50:32+09:00
Xcode archive時にimport不可のエラー
はじめに
FlutterでAPL開発を行い、APPLEの申請を行っていたのですが、思わぬところでつまずいたので、備忘録として保存しておきます。
通常のビルドは、実施可能ですし、シュミレーターの起動や実機での起動も上手く行ってたにも関わらず、appleでの申請用にarchiveしようとしたら内部で使用しているライブラリをimportできないとエラーになりました。この記事を参考にarchiveしており、エラーになりました。この記事は何も悪くはなく、素晴らしい記事です。
試した内容
- クリーンして再ビルド。 → NG
- これを参考に、Podfileを削除して、
pod install
からやり直し。 →NG- これを参考に、deployment target を変更してみる。 →NG
- これを参考に、読み込みのパスを変更。 →NG
結局解消した方法
これを参考にしました。
Runner.xcproject
ではなくRunner.xcworkspace
でXcodeを開いてarchiveする必要があったみたいです。
下記のコマンドで開けるので、お試しください。
open Runner.xcworkspace
結局の原因
これ にこの2つの開き方の違いが記載されているのですが、複数プロジェクトの依存関係を解決できていなかったみたいです。
- 投稿日:2020-02-05T19:12:19+09:00
iOS Safariでselect optionにdisplay:noneが効かない対策
表題のとおり、iOSの場合はselect optionにdisplay:noneは効かない。
ググってるとwrapするといいとのことで、たしかにwrapすると何故か消える。
(wrapするタグにはstyleを一切設定していないのに・・!?)
ただ、個々にちまちまwrapさせるのもめんどくさいので
optionにdisplay:noneをしたら勝手にoptionタグをwrapしてくれれば
今まで通りだしいいやんって思った次第。attrchange.js
https://github.com/meetselva/attrchange/
このjQueryプラグインを利用すると
styleに変化があるとEventが上がってくるので利用させてもらったのが以下のコード。もう少しやり方あるだろ?って思われそうだけど、ひとまずそこは・・
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.1/jquery.min.js"></script> <script src="./Scripts/attrchange.js"></script> <script> $(document).ready(function(){ //iOSのときselect optionをdisplay=noneにしても消えないので、 //attrchange.jsを利用してcssがdisplay=noneとかにいじられたらoptionタグをwrapする $("option").attrchange({ trackValues: true, callback: function(evnt) { //console.log(evnt); if(evnt.type == "attributes" && evnt.attributeName=="style"){ if($(this).css("display")=="none"){ if($(this).parent().attr("mode")!="wrap"){ $(this).wrap("<p mode='wrap'>"); } }else{ // 空白やdisplay iosだとinlineになるっぽい if($(this).parent().attr("mode")=="wrap"){ $(this).unwrap(); } } } } }); }); </script>しかし、なんでoptionをpタグでwrapすると消えるんだろう??
- 投稿日:2020-02-05T15:38:39+09:00
flutter_blueがiOSでヘッダーファイルを見つけられなくてbuildできない問題
エラーメッセージ
fatal error: could not build module 'flutter_blue'あるいは
'FlutterBluePlugin.h' file not foundというエラーが出てビルドできない。
環境
- Flutter 1.14.6
- flutter_blue 0.6.3+1
- cocoapods 1.8.4
- Xcode 11.3.1
対策
Flutter Pluginsにある
flutter_blue-0.6.3+1/ios/flutter_blue.podspec
ファイルの一部を下記のように編集する
(Android Studioで作業しているとプロジェクト外のファイルを触ることで警告が出るかもです。)~ 略 ~ s.subspec 'Protos' do |ss| ss.source_files = 'gen/**/*.pbobjc.{h,m}' # ss.header_mappings_dir = '.' <- コメントアウト ss.requires_arc = false ss.dependency 'Protobuf' end ~ 略 ~iOSのプロジェクトディレクトリにあるPodsやPodfile.lockを削除し、インストールをやり直す。
rm -fr Pods Podfile.lock pod install参考
- 投稿日:2020-02-05T13:12:41+09:00
初めてiOSアプリにとりかかるデザイナーさんへ、私(エンジニア)が楽するためのお願い
はじめに
今回はiOSアプリの開発で、私(エンジニア)がデザインをコードに落とし込む際、どういう解釈をするのかを知ってもらいよりスムーズな開発が行えるようにしたいという目的です。
私がいろいろなプロジェクトでいろいろなデザイナーさんとやりとりしながら感じた、勘所、かゆい所を共有したいと思います。
ここでは、いわゆるiOS HIG(ヒューマンインターフェースガイドライン)を踏まえた上で現場でよく使う用語、役にたった作り方やルールを記載しますので、HIGから学びたい方は、まとめやAppleの公式ページを参照することをおすすめします。
また、文字読むのが苦手な人は、iOSの純正アプリ(AppStoreだけ少々奇抜な気がしますが。)をじっくり見てみることをおすすめします。iOS Human Interface Guidelines
https://developer.apple.com/design/human-interface-guidelines/ios/overview/themes/お願いしたいこと
よく使うiOSのレイアウト用語を知ってください
ローンチスクリーン、起動画面、スプラッシュスクリーン LaunchScreen, SplashScreen
ローンチスクリーン(起動画面)とスプラッシュスクリーンは厳密には違うのですが、混同して使われる現場がしばしばあり、認識としてはどちらも、アプリを立ち上げた直後の画面、でいいと思います。
ローンチスクリーンは広義な方で、文字通り起動画面で、上の画像全てです。
一方スプラッシュ画面は、左2つ(Youtube, Twitter)のような画面を指すことが多く、起動直後に一瞬静止画面(たまにアニメーション)が立ち上がり、コンテンツの画面に自動で遷移する、といったものです。純正アプリはほぼこちらを使用していないようで、コンテンツの背景がいきなり登場します。
Appleがデフォルトで用意しているのは、静止画面(画像や文字を配置)が全画面にふわっと出てくるもので、Twitterのようにロゴがアニメーションするものは実装が少々複雑になります。ステータス/エリア、バー Status/Area, Bar
端末上で時間や通信情報、バッテリー情報を表示している場所です。
大きく、表示・非表示、LightMode・DarkModeをコントロールできます。
アプリケーション全体を通しての設定を一括で行うのが基本ですが、この場合、後述のナビゲーションエリアの色によって自動でステータスエリアの色(light or dark)が決められたりするので、少々実装コストはかかりますがページごとに色をコントロールすることもできます。
色以外のレイアウトを変更することはできません。ナビゲーション/エリア、バー、タイトル、ボタン、遷移 Navigation/Area, Bar, Title, Button, Transition
基本的にはステータスエリアの下にくっつけて配置され、ページのタイトルや戻るボタン、完了ボタンや検索ボタン等、が置かれている場所です。
スマートフォンのUIは1ページで多くの情報を表示するために画面をスクロールさせますが、このナビゲーションは常に最前面に配置されており、画面のスクロールの影響を受けません。
これにより、スクロールして情報が変わっても、画面として普遍なものを配置することが多いです。(余談ですが、これを壊してきたのがTwitter等のアプリで、スクロールに応じてナビゲーションエリアに配置されたものや、文字が変わっていくというギミックを取り入れました)
iOSが用意するNavigationには、いくつか制約があり、ガイドラインに沿った実装は簡単に行えるのですが、少し凝った実装をすると独自に作ったNavigationを使用することになり、トータルの実装コストが上がります。
これはiOSが用意してくれているNavigationがデフォルトでいろいろな機能を実装してくれている(特に遷移の管理)ので、そちらの機能を自前で実装しないといけなくなるからです。タブバー/エリア、ボタン TabBar/Area, Button
最下部にあるボタンが均等に横並びになっている場所です。
デフォルトだと、アイコンと文字が縦並びになったボタンがセットになっており、均等に配置されていて、選択・非選択状態の色も自動で割り当てられます。
背景色、高さ、ボタンサイズ、選択状態の色・画像、タイトルの有無・色・位置・フォント、それぞれ変更可能です。
タブバーのシステム的利点は、タブバーでの画面遷移は他のタブの状態が保持されている、というところにあり、タブを繰り返し切り替えても、タブごとに同じ画面が表示されます。セーフエリア SafeArea
iPhone,iPad端末は上部にノッチがあったり、時間や通信情報を表示していたり、下部にセンターボタンがあったり、HomeIndicatorがあったりします。
そこで、アプリケーションのコンテンツを表示していい領域を、全端末共通でわかりやすく区切った範囲がこのセーフエリアとなります。
端末によってセーフエリアの見た目は違うのですが、例えばシステム上でセーフエリアの左上から座標を指定すると各端末に合わせた位置に配置してくれるようになるわけです。
セーフエリア自体は外すこともできますが、設定されている場合、デフォルトでは前述のステータスエリアやHomeIndicatorが省かれたエリアになっていますが、ナビゲーションバーをおいたり、タブバーをおくと、そこが省かれたエリアに設定されます。
例えば、ナビゲーションバーを半透明にし、裏でスクロールしているアイテムが見えるようにしたい場合、セーフエリアをステータスエリアまで狭める必要があるわけです。*SafeAreaは言葉や1枚絵だけでは伝えづらいので、Appleやまとめの記事を参照してみてください。
SafeAreaについてまとめ
https://qiita.com/gentlejkov/items/a626263d452939378b07テーブル/ビュー、セル Table/View, Cell
垂直方向スクロールしながら情報を表示することができるレイアウトです。
大きくセクションとセルという単位に別れており、セクションは大項目(図では[A][D]だったり、何も表示されていなかったり)、セルは小項目(図ではセクションとセクションの間の一つずつ)という認識で問題ないと思います。
テーブルビューはiOSのデフォルトで、かなり多くの便利な機能が実装されており、標準から切り離し独自で実装するのはものすごい実装コストが高いです。コレクション/ビュー、セル Collection/View, Cell
テーブルビューより自由にセルを配置でき、垂直、水平どちらにもスクロールして情報を表示することができるレイアウトです。
セクションとセルの概念はテーブルビューと同じで、こちらもiOSデフォルトで便利機能が盛りだくさんです。
テーブルビューもですが、不規則な配置であればあるほど実装の難易度があがります。不規則の代表例として、よくPinterestのTOPページのUIがあげられます。
根本的にテーブルビューより実装コストは高いです。モーダル(遷移、 ビュー) Modal(Transition, View)
前の画面の上に覆いかぶさり、前の画面にユーザーが干渉できないビュー(遷移)というイメージです。
iOS13より、画像のようなモーダル表示を見かけるようになりました。今まで全画面のモーダルは、表示上、他の全画面ビューと変わらず、よく見かける特徴としては、左上に←という戻るボタンではなく×という戻るボタンが表示されており、遷移のアニメーションも下からにょきっと出てくるものが多かった印象です。
iOS13も遷移、戻るボタン等はほぼ同じですが、上部の隙間から前画面がのぞいてるように見える表現がデフォルトになりました。もちろん以前のように全画面表示も可能です。レイアウトの際のパーツ(レイヤー)を意識してください
デフォルト
ここまでも度々使用してきたデフォルトという言葉ですが、iOSは実装する際にレイアウトから画像も含めて標準で備えている(デザインを用意する必要がない)ものがあります。
挙げるときりがないですが、ダイアログ、アクションシート、インディケーター等々、iOSの純正アプリで使われていることが多いです。
デフォルトの実装は軽度で、デフォルトから逸脱すればするほど実装が重度になります。
また、デフォルトのUIでも、色、形、位置の変更の難易度がそれぞれ違ったりします。慣れてくると、Appleのこういうニュアンスで使ってほしいという意志を感じ取って、そこからの距離感で実装難易度を予想することができます。
逆に、iOSの純正アプリが実装していることは、やってやれないことはないはずです。(稀に例外あり)レイアウトのサイズ、座標
レイアウトのサイズ、座標は共に整数(可能であれば2の倍数)です。
レイアウトは座標をとりやすいように、サイズ、座標ともに配置してください。サイズに関しては透明な余白を設ける方法がよく使用されます。
モノ(オブジェクト)、レイヤーの単位を意識して配置してください。ステータスエリア、ナビゲーションエリア、テーブルビューのセル、左隣のボタン、上のテキスト、X軸の中央、Y軸の中央等々。特に頻出するテーブルビューはセル単位でレイアウトを配置するようにしてください。
1つのアプリケーション内の様々なページに頻出するレイアウトというものが出てくると思いますが、これはデザインでも実装でも同じく再利用する可能性があります。この再利用という単位を意識することが大事です。ボタンのタップ領域
様々なボタンが出てくると思いますが、ガイドラインにある通り、ボタンのタップ領域は最低44x44ptじゃないといけません。
これは実際やってみるとわかりますが、指でストレスなく押せる最低限のサイズです。
また、ボタンが画像である場合、画像の切り出し方ですが、可能な限りタップ領域を考慮した切り出しをしていただけると助かります。
よく使われる手法は、表示しない部分は透明な状態で画像に組み込む方法です。画像等リソースの管理を意識してください
命名規則
・フォルダ名、ファイル名は、以下2種類のみ使用します。
- 半角英数字
- _(アンダースコア)
・新しく画像を作成する際に命名に困ったら、私(エンジニア)に相談してください。
・命名の法則を決めて、共有してください。
参考:よく使っている画像の命名規則 -> ファイル名のルール
https://qiita.com/manabuyasuda/items/675586be79c4a8eebbfc共有管理
・ツールによって画像のバージョン管理(変更履歴の管理)ができているなら問題ないが、画像の変更の伝達ミスを避けるため、日付(時間)のフォルダを作り、変更したものを入れて共有するとやりやすいです。
・画像のサイズは基本的に変更しないようにし、どうしても変更する場合は、私(エンジニア)に漏れなく伝えるようにしてください。
・例えばページの名前(ホーム画面、検索画面等)、パーツの名前等のモノの呼称はチームで共通認識化してください。
・画像の名前は変更しないでください。
・不要になった画像も管理してください(可能であれば認識し、私(エンジニア)に伝達してください)。
おわりに
今回挙げたいくつかの事柄には、デザインの幅を狭めるものがあると思います。
なので、あくまで私(エンジニア)が楽する、もとい開発速度を上げるための手法というのを念頭においてください。時間をかけても実現したいデザインがある場合には、これにとらわれる必要はないと思いますし、挑戦的なデザインは時間に余裕があれば大好物です。
この記事で全てを網羅しているわけではないので開発途中で必要に応じて認識合わせを行なっていくことが大事だと思います。
その際のベースの考え方となれば幸いです。できあがったデザインを見て、レイアウトを把握して、そこに込められた意志を感じながら実装するのが好きです!
- 投稿日:2020-02-05T10:49:55+09:00
[Swift5]ニコニコ動画やLINEにあるようなスクロールによって閉じたり開いたりするヘッダー
はじめに
TableViewのスライドに同期して出てきたり閉じたりするヘッダーみたいなバーは、主に検索やビューの切り替えなどに使われ非常に便利なツールです。
簡単に実装できないかなぁとネットを探しまわったんですが、思い通りの情報が得られず結局自分で実装したので共有します。
少し複雑な内容になっていますが、初心者の人にも出来るだけ分かりやすく解説したいと思います。
意外と重宝しそうなツールほどネットに転がっていなかったりしますよね。
この記事が皆さんの助けになれば幸いです。!初投稿かつSwift初心者なので至らない点があるかもしれません。
(この記事を気に入った人は"いいね"して頂けるとモチベーション上がります)目次
- 環境
- 実行例
- 考え方
- 必要な値を取得
- 適切なヘッダーの状態を判断する
- ヘッダーの状態を元にヘッダーに対してスクロール処理をする
- 必要な値を更新する
- ソースコード
- コードを紐解く
- おわりに
環境
- Xcode 11.2.1
- Swift5
実行例
考え方
はじめにTableViewのスクロールに連動して動くツールバーみたいなものをこの記事ではヘッダーとして呼びます。
基本的な考え方としては、TableViewにおけるスクロールの向きと量に基づいてヘッダーの座標を動かすことになります。
実際に行われる処理は以下の表のような流れになります。
順番 処理内容 1 必要な値を取得する 2 1で得た値をもとに適切なヘッダーの状態を判断する 3 2で得たヘッダーの状態をもとにヘッダーに対してスクロール処理を実行する 4 必要な値を更新する この流れに沿って説明したいと思います。
また、今回説明するビュー(初期状態)を示した図を下に載せておきます。
ヘッダーはヘッダービューの一部であることが分かりますね。
余談ですが、ヘッダーの上に余白を大きく持たせているのはローディングの際にアニメーションを見せるためです。
(要望があればこちらも記事にします。)ScrollViewの上の余白をヘッダーの高さ分設けていることに注意してください。
1.必要な値を取得する
以下が実際に呼び出されるイベント関数となっています。
ここで私たちがこれから定義していく関数を呼び出すことになります。func scrollViewDidScroll(_ scrollView: UIScrollView) { var subSet: [String:CGFloat] = ["up": 0, "down": 0] // MARK: -- 以下のinitialHeaderFrameに適切な値を入力するだけで、このプログラムは動作します。 let initialHeaderFrame: [String:CGFloat] = ["Y": -260, "height": 60] let headerFrame: [String:CGFloat] = ["minY": initialHeaderFrame["Y"]! , "maxY": initialHeaderFrame["Y"]! + initialHeaderFrame["height"]!, "height": initialHeaderFrame["height"]!] subSet["up"]! += (lastContentOffset - scrollView.contentOffset.y)*0.1 subSet["down"]! += (lastContentOffset - scrollView.contentOffset.y)*0.1 let status = self.getHeaderViewStatus(&subSet, scrollView.contentOffset.y, myHeaderView.frame, lastContentOffset, headerFrame) self.scrolling(status: status) lastContentOffset = scrollView.contentOffset.y }必要な値について述べたいと思います。
- スクロールビューにおける現在のy座標 (
scrollView.contentOffset.y
)- スクロールビューにおける過去(更新前)のy座標 (
lastContentOffset
)- ヘッダービューのx, y, width, height情報 (
myHeaderView.frame
)- 過去と現在のスクロールビューにおけるy座標の差(スクロール量) (
subset:[String:CGFloat]
)- ヘッダービューの初期座標と高さ (
initialHeaderFrame: [String:CGFloat]
)注意点としては、Swiftは左上が原点となっている座標系であることです。
コード上では過去と現在のスクロールビューにおけるy座標の差は
(lastContentOffset - scrollView.contentOffset.y
)
のように計算されます。
特筆すべき点としては、subset[String:CGFloat]
には同じ値を"up"
と"down"
をキーにして二つ保存しています。
これは、上下のスクロールごとに値を用意するためです(理由は後述)。
少し冗長に感じますが便利です。また、よく使う値としてヘッダービューのデフォルトy座標をヘッダーの高さ分下げたy座標があります。
そちらも、この関数内であらかじめ用意しておくことになります。
(コード内のheaderFrame["maxY"]
のことです)2.得た値をもとに適切なヘッダーの状態を判断する
まずヘッダーの状態について説明します。
今回はプログラムを分かりやすくするために5つの状態を用意しました。
状態 説明 start 初期状態、ビューのトップにヘッダーが存在している状態 move_up 下向きにスクロールしていて、ヘッダーが全て見えきっていない状態 stop_up 下向きにスクロールしていて、ヘッダーが全て見えきっている状態 move_down 上向きにスクロールしていて、ヘッダーが全て隠れきっていない状態 stop_down 上向きにスクロールしていて、ヘッダーが全て隠れきっている状態 これらは全て(関数)列挙型で表現されています。
enum headerViewStatus { case start(_ headerViewFrame: CGRect, _ initHeaderFrameMinY: CGFloat) case move_up(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect) case stop_up(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMaxY: CGFloat) case move_down(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect) case stop_down(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMinY: CGFloat) }さて、次にヘッダーの状態を判断する関数(
getHeaderViewStatus
)を見ていきましょう。
上で述べたとおり5つの状態を判断する式を持っています。5つの状態を判断する条件式をA, B, C, Dとおくと以下のフローチャートのように整理できます。
条件式 簡易的な意味 A 初期状態かどうか B 下向きにスクロールをしたかどうか C ヘッダーが全て表示される位置に移動しているかどうか D ヘッダーを全て隠れる位置に移動しているかどうか 以下はヘッダーの状態を判断する関数のコードです。
private func getHeaderViewStatus(_ sub: inout[String:CGFloat], _ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ lastScrollViewY: CGFloat, _ initHeaderFrame: [String:CGFloat]) -> headerViewStatus { if (scrollViewY <= (0 - initHeaderFrame["height"]!)) { return headerViewStatus.start(headerViewFrame, initHeaderFrame["minY"]!) } else if (lastScrollViewY > scrollViewY) { sub["down"] = 0 if(headerViewFrame.origin.y >= scrollViewY + initHeaderFrame["maxY"]!) { return headerViewStatus.stop_up(scrollViewY, headerViewFrame, initHeaderFrame["maxY"]!)} else { return headerViewStatus.move_up(sub["up"]!, scrollViewY, headerViewFrame)} } else { sub["up"] = 0 if(headerViewFrame.origin.y <= scrollViewY + initHeaderFrame["minY"]!) { return headerViewStatus.stop_down(scrollViewY, headerViewFrame, initHeaderFrame["minY"]!)} else { return headerViewStatus.move_down(sub["down"]!, scrollViewY, headerViewFrame)} } }
条件Aは以下のコードで表現されています。
if (scrollViewY <= (0 - initHeaderFrame["height"]!))スクロールビューのy座標が、ヘッダーの高さ分だけ小さい位置に配置されているかについての条件式です。
初期状態ではスクロールビューはヘッダーの高さ分だけ上に配置されていますよね。
なので初期状態、もしくはそれより上に画面を進めた(下向きへスクロールした)場合ではTrueとなり、
それ以外ではFalseとなり次の条件式に進んでいきます。参考にビューの初期状態を再掲します。
条件Bは以下のコードで表現されています。
if(lastScrollViewY > scrollViewY)前回スクロールした際のスクロールビューのy座標の方が現在のy座標よりも下にあるか(大きいか)どうかについての条件式ですね。
この条件式がTrueになるのは、ビューを下にスクロールして画面を上に進めた場合でしょう。つまりこの条件式はビューを下にスクロールしたかどうかについての条件式となります。
例として上にスクロールした場合のビューの様子を以下の図で示しました。
条件Cは以下のコードで表現されています。
if(headerViewFrame.origin.y >= scrollViewY + initHeaderFrame["maxY"]!)ビューを下にスクロールした場合この条件Cを実行することになります。
ビューを下にスクロールした場合はヘッダーを出す必要があります。
しかし、出し過ぎる(ヘッダーを下げ過ぎる)のも問題ですよね。
画面の一番上にくっついているように表示したいです。条件Cは画面の一番上にくっついているかどうかについて判断します。
画面のトップの位置、それより下にあればTrueで、
まだ完全に表示されきっていないならFalseとなります。以下の図は条件式がTrue(等しくなる)場合のビューの状態を示しています。
まずは変数の説明をしましょう。
headerViewFrame.origin.y
はヘッダービューの現在の座標を示しています。
scrollViewY
はスクロールビューの現在の座標を示しています。
initHeaderFrame["maxY"]
はヘッダービューのデフォルト座標をヘッダーの高さ分下にずらした座標を示しています。ここで、図における
header_height
を30
、
HeaderView_height
を230
、
画面の座標を(0, 100)
とすると赤点の座標(
initHeaderFrame["maxY"]
)=(0, -200)
青点の座標(scrollViewY
)=(0, 100)
橙点の座標(headerViewFrame.origin.y
)=(0, -100)
となります。そして条件式は、
-100 >= 100 + (-200)
よりTrueとなります。これは、(青点+赤点)のy座標が橙点のy座標と同じ位置、もしくはそれ以上ならばヘッダーは全て表示されていることが保証されていることに由来します。
(皆さんも頭の中で青点を-200分動かしてみてください)以下の図は上記の考え方を分かりやすく示したものです。(左の状態でTrue、右の状態でFalse)
青点が二つありますが、上の方にある青点は
-200
された位置にあるものです。(仮想青点と呼びましょうか)
画面をスクロールしてヘッダーを仕舞う(図の右の状態に遷移)と橙点が移動したのが分かりますね。この橙点が仮想青点よりも上にあるとFalseで、橙点が仮想青点と重なる(もしくは下に位置する)とTrueとなります。
この式によって、ヘッダーが画面に全て表示されているかどうか判断できるのです。
では最後に条件Dについても見ていきましょう。ロジックは全く同じです。
条件Dは以下のコードで表現されています。
if(headerViewFrame.origin.y <= scrollViewY + initHeaderFrame["minY"]!)こちらも先ほど説明した条件Cにとても似ていますね。
不等号が逆になったのと、スクロールビューのy座標に足す値がヘッダービューのデフォルトy座標に変わりました。
(条件式が等しい時にTrueになるよう調節するためです)こちらもビューがどのような状態の時に条件式が等しくなるのか示しておきましょう。
左の図はTrueの場合で、右の図はFalseの場合です。それぞれの点は、
赤点の座標(initHeaderFrame["minY"]
)
青点の座標(scrollViewY
)
橙点の座標(headerViewFrame.origin.y
)
を示しています。この橙点が仮想青点よりも下にあるとFalseで、橙点が仮想青点と重なる(もしくは上に位置する)とTrueとなります。
つまり、ヘッダーが全て画面の上に隠れたかについての条件式です。
以上で全ての適切なヘッダーの状態を判断する条件式の説明を終えました。
こうして見ると、意外と複雑に考える必要があったんだという事が分かりますね。さて、次はヘッダーの状態において適切な処理をしていくことになります。
3.ヘッダーの状態をもとにヘッダーに対してスクロール処理を実行する
まずは、ヘッダーの状態と、その際の適切なスクロール処理について表にまとめました。
状態 適切な処理 start 画面の一番上にくっついているように表示する move_up ヘッダーを下げて表示する stop_up ヘッダーを画面の一番上にくっついているように固定する move_down ヘッダーを上げて画面から退場させる stop_down ヘッダーが画面から見えないギリギリの位置に固定する つまりヘッダーは以下の図の矢印の間を行ったり来たりするように動きます。
スクロールについての処理を行うので、y座標に対する操作だけに注目してください。
注意点としては、ヘッダーではなくヘッダービューに対しての操作を行なっているという事です。
ヘッダーはヘッダービューの一部ですからね。
状態がstartの場合は処理は以下のように記述されています。
func start(_ headerViewFrame: CGRect, _ initHeaderFrameMaxY: CGFloat) { print("Start") myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: initHeaderFrameMaxY, width: headerViewFrame.width, height: headerViewFrame.height) }
initHeaderFrame_maxY
はヘッダーのデフォルトy座標のことなので、ヘッダービューには初期状態がセットされます。
状態がmove_upの場合は処理は以下のように記述されています。
func move_up(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect) { print("Move_up") myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: headerViewFrame.origin.y + sub, width: headerViewFrame.width, height: headerViewFrame.height) }
sub
という変数が出てきましたね。これは スクロール量です。
これは1.必要な値を取得するで述べた通り、subSet
のキー"up"
の値で、以下の計算式で求められます。(
lastContentOffset - scrollView.contentOffset.y
)上向きにスクロールしているので、
lastContentOffset > scrollView.contentOffset.y
です。
したがって、sub
の値は正になることが分かりますね。また、
headerViewFrame.origin.y
は現在のヘッダービューにおけるy座標なので、
(headerViewFrame.origin.y + sub
)
は現在のヘッダービューにおけるy座標をスクロール量分大きくしている(下げている)ことが分かります。
状態がstop_upの場合は処理は以下のように記述されています。
func stop_up(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMaxY: CGFloat) { print("Stop_up") myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: (scrollViewY + initHeaderFrameMaxY), width: headerViewFrame.width, height: headerViewFrame.height) }これはスクロールビューのy座標(
scrollViewY
)に常にヘッダーのデフォルトy座標+ヘッダーの高さ(initHeaderFrameMaxY
)を足していますね。条件Cの処理を思い出してください。ロジックは全く同じです。
以下の図に青点が二つありますが、上の方が(scrollViewY)+(initHeaderFrameMaxY)
を表している仮想青点です。仮想青点にヘッダービューのy座標を合わせる処理ということですね。
また、この処理が実行されるのは橙点と青点が重なる場合なので、常に図の左側を表示し続けることになります。
つまりヘッダーを常に表示されるように固定しているという事なんですね。
状態がmove_downの場合は処理は以下のように記述されています。
func move_down(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect) { print("Move_down") myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: headerViewFrame.origin.y + sub, width: headerViewFrame.width, height: headerViewFrame.height) }こちらもmove_upの場合の処理と同じです。
違いはsub
が負の値になっている事ですね。スクロールの向きに由来するものです。
状態がstop_downの場合は処理は以下のように記述されています。
func stop_down(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMinY: CGFloat) { print("Stop_down") myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: (scrollViewY + initHeaderFrameMinY), width: headerViewFrame.width, height: headerViewFrame.height) }こちらもstop_upの場合の処理と同じように、この処理にたどり着くまでの条件をみていくと分かります。
stop_downの場合は条件Dを参照してください。
この処理はヘッダーを常に見えないギリギリの位置に固定していることになります。
必要な値を更新する
最後はプログラムに必要な値を更新する処理です。
と言っても、明示的に行うのは以下のたった一行のコードです。
lastContentOffset = scrollView.contentOffset.y現在のスクロールビューのy座標を次のサイクルで使えるように、更新しているだけですね。
さて、ここまでで論理的なプログラムの説明を終わります。
お疲れ様でした。ソースコード
とりあえず、ポイントとなるプログラムは以下に載せておきます。
Githubにサンプルを上げておくので、そちらも参考にされてみてください。https://github.com/Hajime-Ito/HeaderTestSwift
var lastContentOffset: CGFloat = 0 enum headerViewStatus { case start(_ headerViewFrame: CGRect, _ initHeaderFrameMinY: CGFloat) case move_up(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect) case stop_up(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMaxY: CGFloat) case move_down(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect) case stop_down(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMinY: CGFloat) } private func getHeaderViewStatus(_ sub: inout[String:CGFloat], _ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ lastScrollViewY: CGFloat, _ initHeaderFrame: [String:CGFloat]) -> headerViewStatus { if (scrollViewY <= (0 - initHeaderFrame["height"]!)) { return headerViewStatus.start(headerViewFrame, initHeaderFrame["minY"]!) } else if (lastScrollViewY > scrollViewY) { sub["down"] = 0 if(headerViewFrame.origin.y >= scrollViewY + initHeaderFrame["maxY"]!) { return headerViewStatus.stop_up(scrollViewY, headerViewFrame, initHeaderFrame["maxY"]!)} else { return headerViewStatus.move_up(sub["up"]!, scrollViewY, headerViewFrame)} } else { sub["up"] = 0 if(headerViewFrame.origin.y <= scrollViewY + initHeaderFrame["minY"]!) { return headerViewStatus.stop_down(scrollViewY, headerViewFrame, initHeaderFrame["minY"]!)} else { return headerViewStatus.move_down(sub["down"]!, scrollViewY, headerViewFrame)} } } private func scrolling(status: headerViewStatus) { func start(_ headerViewFrame: CGRect, _ initHeaderFrameMaxY: CGFloat) { print("Start") myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: initHeaderFrameMaxY, width: headerViewFrame.width, height: headerViewFrame.height) } func move_up(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect) { print("Move_up") myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: headerViewFrame.origin.y + sub, width: headerViewFrame.width, height: headerViewFrame.height) } func stop_up(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMaxY: CGFloat) { print("Stop_up") myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: (scrollViewY + initHeaderFrameMaxY), width: headerViewFrame.width, height: headerViewFrame.height) } func move_down(_ sub: CGFloat, _ scrollViewY: CGFloat, _ headerViewFrame: CGRect) { print("Move_down") myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: headerViewFrame.origin.y + sub, width: headerViewFrame.width, height: headerViewFrame.height) } func stop_down(_ scrollViewY: CGFloat, _ headerViewFrame: CGRect, _ initHeaderFrameMinY: CGFloat) { print("Stop_down") myHeaderView.frame = CGRect(x: headerViewFrame.origin.x, y: (scrollViewY + initHeaderFrameMinY), width: headerViewFrame.width, height: headerViewFrame.height) } switch status { case let .start(headerViewFrame, initHeaderFrameMinY): start(headerViewFrame, initHeaderFrameMinY) case let .move_up(sub, scrollViewY, headerViewFrame): move_up(sub, scrollViewY, headerViewFrame) case let .stop_up(scrollViewY, headerViewFrame, initHeaderFrameMaxY): stop_up(scrollViewY, headerViewFrame, initHeaderFrameMaxY) case let .move_down(sub, scrollViewY, headerViewFrame): move_down(sub, scrollViewY, headerViewFrame) case let .stop_down(scrollViewY, headerViewFrame, initHeaderFrameMinY): stop_down(scrollViewY, headerViewFrame, initHeaderFrameMinY) } } func scrollViewDidScroll(_ scrollView: UIScrollView) { var subSet: [String:CGFloat] = ["up": 0, "down": 0] // MARK: -- 以下のinitialHeaderFrameに適切な値を入力するだけで、このプログラムは動作します。Yにはヘッダービューのy座標を、heightにはヘッダーの高さを入力してください。 let initialHeaderFrame: [String:CGFloat] = ["Y": -260, "height": 60] let headerFrame: [String:CGFloat] = ["minY": initialHeaderFrame["Y"]! , "maxY": initialHeaderFrame["Y"]! + initialHeaderFrame["height"]!, "height": initialHeaderFrame["height"]!] subSet["up"]! += (lastContentOffset - scrollView.contentOffset.y)*0.1 subSet["down"]! += (lastContentOffset - scrollView.contentOffset.y)*0.1 let status = self.getHeaderViewStatus(&subSet, scrollView.contentOffset.y, myHeaderView.frame, lastContentOffset, headerFrame) self.scrolling(status: status) lastContentOffset = scrollView.contentOffset.y }コードを紐解く
ではコードを紐解いていきましょう。
全部説明する必要もないと思うので、特筆すべき部分だけにしときます。
1つ目は参照渡しですね。
getHeaderViewStatus
の引数に注目してください。private func getHeaderViewStatus(_ sub: inout[String:CGFloat], ....)引数
sub
がinout
になっていますね。
これは参照渡しを明示的に行なっているという意味です。通常、Swiftでは関数に渡す引数は値渡しとなります。
しかし、この関数では
sub
に対する操作も行いたいので、参照渡しをすることになります。
2つ目はsub[String:CGFloat]についてです。
これは先ほどの参照渡しの話と関係しています。
sub
はスクロール量をもつ辞書型の変数でしたね。
それぞれ、up
とdown
をキーとして同じ値を持っていました。これはそれぞれ上下にスクロールを始めてからのスクロール量を計算するためです。
そのために、
getHeaderViewStatus
関数内において、
上にスクロールを始めたら、sub["down"]
を初期化してsub["up"]
の値を利用する
下にスクロールを始めたら、sub["up"]
を初期化してsub["down"]
の値を利用する
ようなプログラムになっているんですね。そして初期化処理を行うために参照渡しにしていた訳ですね。
以上で特筆すべきコードの説明を終わります。
おわりに
以上で終わりとなります。
理解するのに以外と頭をひねる必要があったかも知れませんね。
スクロール方向と画面の進む向きが逆になっていたりするからでしょうか?また機会があれば、他に役に立ちそうなことも記事にしていきたいと思います。
- 投稿日:2020-02-05T09:52:26+09:00
【iOS】書類スキャンライブラリ「WeScan」の紹介と日本語化など
WeScanとは
WeScan makes it easy to add scanning functionalities to your iOS app! It's modelled after UIImagePickerController, which makes it a breeze to use.
https://github.com/WeTransfer/WeScan
このように、カメラやローカルの画像を取り込んで書類っぽい部分をcropできるライブラリ。例えばEvernote Scannableの撮影機能に近い。
2020年に入っても活発に開発されており、いまは紹介映像よりもう少しリッチになっている。
使い方
インストール
Podfileに
pod 'WeScan'
を追加してpod install
。カメラを起動してスキャン
ImageScannerControllerのインスタンスを作ってDelegateを設定、presentすると紹介映像のような画面が出てくる。
import UIKit import WeScan // ←忘れずに class MyViewController: UIViewController { @IBOutlet weak var imageView: UIImageView! func startCamera() { let scannerViewController = ImageScannerController() scannerViewController.imageScannerDelegate = self present(scannerViewController, animated: true) } }ImageScannerControllerDelegateを使ってスキャンの結果を取得できる。
extension MyViewController: ImageScannerControllerDelegate { func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error) { print(error) } func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithResults results: ImageScannerResults) { let croppedImage = results.croppedScan.image // UIImage imageView.image = croppedImage scanner.dismiss(animated: true) } func imageScannerControllerDidCancel(_ scanner: ImageScannerController) { scanner.dismiss(animated: true) } }ライブラリ等から画像を渡してクロッピング
インスタンス作成時に画像を渡すだけ
ImageScannerControllerのインスタンス作成時に画像を渡すと、カメラは起動せずに渡した画像の編集画面となる。
let scannerViewController = ImageScannerController(image: image, delegate: self) // どこかから画像を渡す。ついでにdelegateも設定 present(scannerViewController, animated: true)iOS標準のUIImagePickerControllerで画像を持ってきて渡す例
class MyViewController: UIViewController { func openLibrary() { let imagePicker = UIImagePickerController() imagePicker.delegate = self imagePicker.sourceType = .photoLibrary present(imagePicker, animated: true) } } extension MyViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true) } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { picker.dismiss(animated: true) guard let image = info[.originalImage] as? UIImage else { return } let scannerViewController = ImageScannerController(image: image, delegate: self) //❗️選んだ画像をWeScanに渡す present(scannerViewController, animated: true) } } extension MyViewController: ImageScannerControllerDelegate { //❗️ここは前と同じ func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error) { print(error) } func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithResults results: ImageScannerResults) { let croppedImage = results.croppedScan.image imageView.image = croppedImage scanner.dismiss(animated: true) } func imageScannerControllerDidCancel(_ scanner: ImageScannerController) { scanner.dismiss(animated: true) } }日本語化とカスタマイズ
すごく良いライブラリなのだが、ちょっと変更したいところもあって…
また、今回の用途だとクロップ設定の初期範囲が少し狭いのが気になった。
Fork
ForkしたリポジトリをPodfileに登録して使う方法は以下に単体で記事を作りました。
【CocoaPods】iOSのライブラリをForkして使う日本語化
ForkしたWeScanをXcodeで開く。
↓選択
↓選択
↓+を押す
↓選択
↓チェック
↓選択
WeScan/Localizable.strings以下にJapaneseが追加されるので、例えば以下のように編集。"wescan.edit.button.next" = "次へ"; "wescan.edit.title" = "切り取り"; "wescan.review.title" = "プレビュー"; "wescan.scanning.cancel" = "キャンセル"; "wescan.scanning.auto" = "オート"; "wescan.scanning.manual" = "マニュアル";初期クロップ範囲を変更する
このあたりを
WeScan/Edit/EditScanViewController.swift/// Generates a `Quadrilateral` object that's centered and one third of the size of the passed in image. private static func defaultQuad(forImage image: UIImage) -> Quadrilateral { let topLeft = CGPoint(x: image.size.width / 3.0, y: image.size.height / 3.0) let topRight = CGPoint(x: 2.0 * image.size.width / 3.0, y: image.size.height / 3.0) let bottomRight = CGPoint(x: 2.0 * image.size.width / 3.0, y: 2.0 * image.size.height / 3.0) let bottomLeft = CGPoint(x: image.size.width / 3.0, y: 2.0 * image.size.height / 3.0) let quad = Quadrilateral(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft) return quad }このように変更すると、
let topLeft = CGPoint(x: 0, y: 0) let topRight = CGPoint(x: image.size.width, y: 0) let bottomRight = CGPoint(x: image.size.width, y: image.size.height) let bottomLeft = CGPoint(x: 0, y: image.size.height)Podfile
コードを書き換えたらpushし、自分のアプリに戻ってPodfileを編集。
repoとbranchを指定してpod installすれば完了。pod 'WeScan', :git => 'git@github.com:{{ your name }}/WeScan.git', :branch => '{{ your branch }}'