- 投稿日:2021-01-16T22:11:31+09:00
写真を読み込んで手書きスケッチ。zoom機能付。作ってみた。
はじめに
写真に手書きスケッチを描きこむツールは、いくつか見つかります。
しかし、ズーム機能のついたツールが見つかりませんでした。
標準のフォトやマークアップは拡大率に限界があって 使いにくかった。
だから、今回作ってみました。
uiもスクリプトで作成してみたので、1ファイルで動きます。コードをバンバン書ける方には 怒られそうな程注釈を書き込んでいます。
自分用のメモですが、Python勉強し始めの方には スクリプトの動作が追いやすい
とおもって残しています。
あと、試行錯誤の結果 使っていない関数が少し眠っています。クラスを始めて使って書いてみました。
環境
ipad + pythonista3
要件
・写真を読み込んで、手書きスケッチを書き込める。
・線の色・太さは 数種類づつ用意した。
・undo機能も実装した。
・拡大縮小(zoomin zoomout)機能の実装。zoomは左上を起点にされます。
・拡大機能の実装に合わせてscrollviewを実装。苦労した点
・scrollviewの実装方法とスクロールと拡大縮小の連携。
・touchイベントとsxrollviewでタッチイベントの受け取りが競合する。
・undoの実装(手書きラインを配列に保管して、最後のラインを消去。その後 全線を再作画)コード
epaint.py#! python3 # 画像読み込んで、拡大機能付スケッチできる # # # 20201228 ver000 easypaint作成開始。 # 20201229 ver001 scrollviewに大きな画像を見て表示してスクロールを実装。 # 20201231 ver002 bottonmenu追加。座標など表示追加。 # 20210101 ver003 pathを別クラスで作成。touchイベントは scrollviewとpathで競合する。最前面のみ受け取る。 # 20210101 ver004 btn_lock他改。touch競合させないためにscvとpvのタッチ有効を切り替える+フレームそろえてpvの座標補正。 # 20210101 ver005 path作画に色と幅を反映させた。pathの作画位置は左上に飛ぶまま。loadは何バージョンか前に実装済み。 # 20210102 ver006 path作画がスクロールしても左上に飛ばないように修正。pvにoffsetを流し込む処理をいれた。 # 20210102 ver007 save作成。 # 20210103 ver008 zoom作成。zoom時の中心位置が左上の少し上と変。左上付近で作画すると全面塗りのエラーが出る。 # 20210105 ver009 タイトルに画像サイズと倍率とファイル名用引数又はepaintを表示。path描画おかしいの修正。 # 20210105 ver010 save画像が倍率で2倍化するのを修正。save完了を表示。 # 20210106 ver011b b版。undo実装。しかし、写真3200x2400サイズでは作画が遅い。 # 20210107 ver012c c版。動作速度改善やundoの太さ等初期化されるバグ修正。コードの注記と旧コード整理。 # 20210110 ver013c c版。scroll後のzoomで作画位置ズレるバグ修正。ボタン位置修正。 # 残 # # 読み込んだ写真ファイル名の取得は、断念。中間ファイル名しか取得できない。元ファイル名+アルファで保存したい。 # QRコードを埋め込んで、元ファイル名渡すのは断念。QRコードのデコードがカメラ経由しかない。 # 画像にステガノグラフィー埋込で元ファイル名を渡すのは、動作スピードからイマイチ。スレッドを分けて作業する? # # import ui, os, sys, photos, scene import Image, io import datetime def pil2ui(imgIn): # pil(jpg) => ui(PNG) pilとios(ui)の使う画像データは異なるので 変換が必要。今回はpil→uiに変換してる。imgIn=pil with io.BytesIO() as bIO: # pilの画像データをiosのuiで使える画像データに変換する。import io必要 imgIn.save(bIO, 'PNG') imgOut = ui.Image.from_data(bIO.getvalue()) del bIO return imgOut ######################################## #手描きを画面に作画。SCROLLVIEWとは競合するので、別classにする必要がある。最前面のみ動く。 class PathView (ui.View): def __init__(self): #self.frame = frame self.flex = 'WH' self.frame=(0,40,1000,680) self.color = 'red' self.path_width = 3 self.path_color = 'red' self.action = None self.touch_enabled = True self.path = None self.paths = [] self.bpath = None self.bpaths= [] self.scvoffset_x = 0 self.scvoffset_y = 0 self.scvrate = 1.0 self.image = None self.base_image = None self.image_w = 256 self.image_h = 256 def touch_began(self, touch): x, y = touch.location x2 = (self.scvoffset_x + x)/self.scvrate y2 = (self.scvoffset_y + y)/self.scvrate print('touch_began x '+str(x)+' y '+str(y)) print(' x2:'+str(x2)+' y2:'+str(y2)+' rate'+str(self.scvrate)) print('scv offset x'+str(self.scvoffset_x)+' y'+str(self.scvoffset_y)) self.path = ui.Path()#画面描画用 self.path.line_width = self.path_width#この一文は幅の初期値設定のみ。固定値でOK。 self.path.line_join_style = ui.LINE_JOIN_ROUND self.path.line_cap_style = ui.LINE_CAP_ROUND self.path.line_width = 3# self.path.move_to(x, y) self.bpath=ui.Path()#画像に描画用 self.bpath.line_width = self.path_width#この一文は幅の初期値設定のみ。固定値でOK。 self.bpath.line_join_style = ui.LINE_JOIN_ROUND self.bpath.line_cap_style = ui.LINE_CAP_ROUND self.bpath.line_width = 0#画面上に描画しない様に幅0とする。 self.bpath.move_to(x2, y2) def touch_moved(self, touch): x, y = touch.location x2 = (self.scvoffset_x + x)/self.scvrate y2 = (self.scvoffset_y + y)/self.scvrate #print('touch_moved x ;'+str(x)+' y'+str(y)) #print('touch_moved x2;'+str(x2)+' y'+str(y2)) self.path.line_to(x, y)#これ実行しないと画像に描画されない。が、実行すると画面に一時的に描画される。 self.bpath.line_to(x2,y2)#画面描画用と画像描画用両方実行し、画像描画用を幅0で非表示。描画時に幅を渡す。 self.set_needs_display() def touch_ended(self, touch): print('touch_ended') #bpathsにpathを保存して、undo実装に利用する。pathは経路のみなので、線色と線幅も記録する。 self.bpaths.append((self.bpath, (int(self.image_w), int(self.image_h)), self.path_color,int(self.path_width))) # viewにactionを設定して、描画の関数を呼ぶ。acionに登録しないと上位クラスの関数path_actionを呼べなかった。 if callable(self.action): # callableは、関数が実行可能かを調べる。要らないかも? self.action(self)#pvのactionに設定しているので、path_actionを呼んでいる。 # pathを1筆毎に初期化。 self.path = None self.bpath = None self.set_needs_display() #この関数で、タッチを画面に描画している。 def draw(self): if self.path: self.path.stroke() self.bpath.stroke() #undoを実現する時にline_widthをメインプログラム側でいじれなかったのでpvインスタンスで実行 def pv_bpath_undo(self): print('pv_path_undo') bpath = None if callable(self.action): self.action(self)#pvのactionを呼んでいる。path_action bpath = None ######################################## # メインプログラム class epaint(ui.View): def __init__(self,filename1): self.name='easy paint'#viewの名前表示。後程ここに、倍率とxy座標uiを表示する。 self.btn_w=100#ボタンの幅 self.btn_h=40 #ボタンの高さ self.filename1 = filename1 self.path_color = 'red' self.path_width = int(6) self.scvoffset_x = int(0)#元画像に対する描画面左上の原点位置 self.scvoffset_y = int(0)#元画像に対する描画面左上の原点位置 self.scvrate = float(1.0)#元画像に対する描画面の倍率 self.scvrate0= float(1.0)#zoom前の描画倍率保管用 self.image_w,self.image_h=int(0),int(0)#元画像の幅と高さ self.pathimage_x,self.pathimage_y=int(0),int(0)#元画像のパス座標 self.pathscv_x,pathscv_y=int(0),int(0)#描画面のパス座標 self.paths = []#pathを配列にして保管する。(undo用) self.path=None self.base_image = ui.Image.named('test:Peppers')#元画像 self.image_w,self.image_h = self.base_image.size#元画像の幅と高さpil.sizeメソッド w,h = ui.get_screen_size()#画面サイズを取得。xとかクラス名除く部分。 self.scr_w,self.scr_h = w,h #全画面の幅と高さ self.biv = ui.ImageView()#base image view元画像のビュー self.biv.frame = (0, 0,self.image_w,self.image_h)#元画像のサイズを取得。左上の座標と右下の座標 # 左上絶対座標x,左上絶対座標y,右下x絶対座標,右下y絶対座標 # ui座標 左上を原点に右下に向かって座標が増える。 # scene座標 左下を原点に右上に向かって座標が増える。いわゆる普通のxy座標と同じ。 # self.biv.image = self.base_image self.biv.bg_color = 'red' self.biv.center = (128,128)#viewの中心をどの座標に表示するかを指定。test画像が256x256なので仮に128とした。 self.biv.transform = ui.Transform.scale(1,1)#bivに表示する画像の倍率を指定。x方向とy方向で指定。 #self.biv.image = ui.Image.from_data(pil2ui(photos.pick_image()))#動かない。イメージの形式が違う? self.scv = ui.ScrollView() #フレーム(実際に表示すrscrollviewの窓サイズ)の幅を宣言 self.scvframesize=(0,self.btn_h+3,self.scr_w,self.scr_h-self.btn_h-80) self.scv.frame = self.scvframesize#(0,self.btn_h+3,self.scr_w,self.scr_h-self.btn_h-80) self.scv.content_size = (self.image_w,self.image_h)#元画像を入れる枠のサイズを宣言 #self.scv.flex='WH' # self.scv.scroll_enabled = False#False # スクロールがTrue有効=動く・False無効=停止。デフォルトTrue self.scv.touch_enabled = False#Falseタッチイベントを受け取らない。下のviewにタッチ渡す。 #Trueタッチイベント受け取る。下のviewにタッチ渡さない。 self.scv.add_subview(self.biv)#scvにbivを入れる事で scrollviewの元画像contentsに代入した。 self.add_subview(self.scv)#selfにscvを入れる事で表示させるのはscvと宣言した self.btnscv = ui.ScrollView()#menu buttonのscrollviewを作成 self.btnscv.background_color = '#d9dcff'#薄紫色 self.btnscv.frame = (0,0,self.scr_w,self.btn_h) self.btnscv.content_size = (1300,self.btn_h) self.btnscv.flex = 'W'#幅方向にのみフィックス self.add_subview(self.btnscv) #menu buttonを登録。同じ項目の繰り返しは下記の関数config_buttonで作っている。 #config_button(button, name, frame, title) self.scv_btn_lock = ui.Button() self.config_button(self.scv_btn_lock, 'btn_lock',(5*self.btn_w,0,self.btn_w,self.btn_h), 'Lock') self.scv_btn_load = ui.Button() self.config_button(self.scv_btn_load, 'btn_load',(0*self.btn_w,0,self.btn_w,self.btn_h), 'Load') self.scv_btn_save = ui.Button() self.config_button(self.scv_btn_save, 'btn_save',(1*self.btn_w,0,self.btn_w,self.btn_h), 'Save') self.scv_btn_undo = ui.Button() self.config_button(self.scv_btn_undo, 'btn_undo',(4*self.btn_w,0,self.btn_w,self.btn_h), 'Undo') self.scv_btn_color = ui.Button() self.config_button(self.scv_btn_color, 'btn_color',(2*self.btn_w,0,self.btn_w,self.btn_h), 'Color') self.scv_btn_path_width = ui.Button() self.config_button(self.scv_btn_path_width, 'btn_path_width',(3*self.btn_w,0,self.btn_w,self.btn_h), '-')#線の幅を指定するボタンなので title無 self.scv_btn_zoomin = ui.Button() self.config_button(self.scv_btn_zoomin,'btn_zoomin',(6*self.btn_w,0,self.btn_w,self.btn_h),'Zin') self.scv_btn_zoomout = ui.Button() self.config_button(self.scv_btn_zoomout,'btn_zoomout',(7*self.btn_w,0,self.btn_w,self.btn_h),'Zout') self.lock_switch = 'lock' self.colors = ['white', 'grey', 'red', 'green', 'blue', 'cyan', 'magenta', 'yellow'] self.color_nr = 2 #red 線の初期色を指定 self.path_widths = [3, 6, 12, 24] self.path_w_nr = 1 #6 線の幅が 何pかを指定 self.scv_btn_color.tint_color = self.colors[self.color_nr] #タッチを作画に渡すに必要(画面に作画まで実施)しかし、viewが最手前のみtouchイベントを受け取る。 #つまり、作画とスクロールビューは両立できない。(最前面を入れ替える事で可能.タッチ有効の切替でも可能) self.pv = PathView()#(frame=self.bounds) self.pv.frame = self.scvframesize#(0,self.btn_h,self.scr_w,self.scr_h-self.btn_h) self.pv.action = self.path_action self.pv.color=self.colors[self.color_nr] self.pv.path_width = self.path_widths[self.path_w_nr] self.pv.scvoffset_x = self.scvoffset_x self.pv.scvoffset_y = self.scvoffset_y self.pv.scvrate = self.scvrate self.add_subview(self.pv) self.path_width_change() self.image = None self.set_btn_actions()#btnにactionを追加。(関数読込後しか登録できないので、後追加とした) # self.present('fullscreen') ############################### #btn click no action def def btn_lock(self,sender): self.logp(sender,'btn_lock') #scrollviewの要素修正して、スクロールのロック・解除を #self.scv.scroll_enabld = True#False # スクロールがTrue有効=動く・False無効=停止。デフォルトTrue if self.lock_switch == 'lock': print('btn_lock to scroll') self.lock_switch = 'scroll' self.scv.scroll_enabled = True self.scv_btn_lock.title = 'scroll' #self.pv.send_to_back()#ビューを背面に移動する。(手前のビューが不透明だと見えなくなる) self.scv.touch_enabled = True#Falseタッチイベントを受け取らない。下のviewにタッチ渡す。 self.pv.touch_enabled = False#Falseタッチイベントを受け取らない。下のviewにタッチ渡す。 else: print('btn_lock to lock') self.lock_switch = 'lock' self.scv.scroll_enabled = False self.scv_btn_lock.title = 'lock' #self.scv.send_to_back()#viewを背面に送る。これをすると画面が上面のviewの下に隠れる。 self.scv.touch_enabled = False#Falseタッチイベントを受け取らない。下のviewにタッチ渡す。 self.pv.touch_enabled = True#Falseタッチイベントを受け取らない。下のviewにタッチ渡す。 self.scvoffset_x,self.scvoffset_y = self.scv.content_offset self.pv.scvoffset_x = self.scvoffset_x#pvの中に現在のscvoffsetを流し込む。 self.pv.scvoffset_y = self.scvoffset_y def btn_load(self,sender): print('btn_load') self.scvrate = 1 self.scvrate0 =1 self.base_image = ui.Image.from_data(photos.pick_image(raw_data=True)) self.image_w,self.image_h = self.base_image.size#元画像の幅と高さpil.sizeメソッド self.pv.image_w , self.pv.image_h = self.image_w , self.image_h self.biv.frame = (0, 0,self.image_w,self.image_h)#元画像のサイズにbivを変更 self.scv.content_size = (self.image_w,self.image_h)#元画像を入れる枠のサイズを宣言 w,h = ui.get_screen_size()#画面サイズを取得。xとかクラス名除く部分。 self.scr_w,self.scr_h = w,h #全画面の幅と高さ self.scvrate0 = self.scvrate #*2 self.biv.image = self.base_image#元画像をbivに代入。 self.zoom_set(sender) #titleに画像サイズと倍率を表示 self.name ='Size:' + str(int(self.image_w)) + ', ' + str(int(self.image_h))+' rate:'+str(self.scvrate)+' '+self.filename1 self.set_needs_display() def btn_save(self,sender): print('btn_save') saveimage0 = self.ui2pil(self.biv.image) nitiji_now = datetime.datetime.now() # 現在日時の取得 file_nitiji = nitiji_now.strftime("%Y%m%d_%H%M%S" )#pillowはファイル名に全角は使えない。 # datetime関数から日時を取り出して、表示する文字の変数に代入。ファイル名にいきなり入れると処理落ちする事がある filename = file_nitiji+'.jpg' print(str(self.image_w)+' , '+str(self.image_h)) #pil risize int必須。ANTIALIASは、性能重視で遅い。 saveimage = saveimage0.resize((int(self.image_w),int(self.image_h)),Image.ANTIALIAS)#for PIL #zoom関係なく、xy各々2倍になるので、元のサイズに縮小している。 saveimage.save(filename, quality=95, optimize=True, progressive=True)#for PIL # カメラロールに保存する前にpyのフォルダに画像を保存する必要がある。 filename2 = 'MemoCamera'+str(self.filename1)+str(file_nitiji)+'Draw.jpg' #filename1=nyuuryoubunn クラス呼出時のコンストラクタに追加した引数 os.rename(filename,filename2)#pillowの代わりに全角ファイル名を作成 #print('Draw_filename2:',filename2) photos.create_image_asset(filename2) # カメラロールへの保存。 # ファイル名は以前に保存した画像ファイルへのパス。保存名ではない。 #file deleate os.remove(filename2) # pyフォルダに生成されたJPEGを消さないと、最初のファイルのタイムスタンプが # 写真のexifデータに継承され続ける為、pyフォルダのjpgは毎回消す。 #titleに画像サイズと倍率を表示 self.name ='save success' #self.close() #保存したらスクリプトを閉じたいときに有効化する。 def btn_undo(self, sender): print('btn_undo') self.path_undo(sender) self.set_needs_display() def btn_color(self, sender): if self.color_nr < len(self.colors) - 1: self.color_nr += 1 else: self.color_nr = 0 self.scv_btn_color.tint_color = self.colors[self.color_nr] self.path_color = self.colors[self.color_nr] self.pv.path_color = self.path_color self.path_width_change() def btn_path_width(self, sender): if self.path_w_nr < len(self.path_widths) - 1: self.path_w_nr += 1 else: self.path_w_nr = 0 self.path_width = self.path_widths[self.path_w_nr] self.pv.path_width = self.path_width self.path_width_change() def btn_zoomin(self,sender): print('btn_zoomin') self.scvrate0= self.scvrate self.scvrate = self.scvrate * 2 self.zoom_set(sender) self.logp(sender,'zoomin') def btn_zoomout(self,sender): print('btn_zoomout') self.scvrate0= self.scvrate self.scvrate = self.scvrate * 0.5 self.zoom_set(sender) self.logp(sender,'zoomout') ############################### # sub tool def pil2ui(imgIn): # pil(jpg) => ui(PNG) pilとios(ui)の使う画像データは異なるので 変換が必要。今回はpil→uiに変換してる。imgIn=pil with io.BytesIO() as bIO: # pilの画像データをiosのuiで使える画像データに変換する。import io必要 imgIn.save(bIO, 'PNG') imgOut = ui.Image.from_data(bIO.getvalue()) del bIO return imgOut #from pythonista forum def ui2pil(self, image): mem = io.BytesIO(image.to_png()) out = Image.open(mem) out.load() mem.close() return out # ボタンをまとめて設定する関数。同じ属性の繰り返し登録なので 関数化してる。 def config_button(self,button,name,frame,title): button.name = name button.frame = frame button.title = title button.border_width = 1 button.corner_radius = 2 button.border_color = 'blue' button.font = ('<system-bold>',25) #button.action = name#ボタン押した時の関数はボタン名称と同じにした。 #しかし、コールする関数を先に読み込む必要があるので、ここでは追加できない。後で別の関数で追加。 self.btnscv.add_subview(button)#menu buttonのscrollview=btnscvにボタンを元ビューとして代入。 #ボタンのアクションは、アクションの関数を登録していないと宣言できないので、最後に登録する def set_btn_actions(self): for subview in self.btnscv.subviews:#ボタンの入っているviewを指定する if isinstance(subview, ui.Button): subview.action = getattr(self, subview.name) # imageviewに、加工後の写真を表示する。 def imgv_pick(): imgIn = photos.pick_image() imggg = pil2ui(imgIn) # 下記でpil→uiに画像形式変換してる。 #sender.superview['photo1'].image = imggg # imegeviewのphoto_nowにios(ui)形式画像を渡して、反映。 return imggg # ウィンドウサイズに画像を適当にfitさせてくれる。 ############################## # draw tool #path_width buttonの線幅画像を変更する def path_width_change(self): with ui.ImageContext(self.btn_w, self.btn_h) as ctx: ui.set_color(self.colors[self.color_nr]) path = ui.Path() path.line_width = self.path_widths[self.path_w_nr] path.line_join_style = ui.LINE_JOIN_ROUND path.line_cap_style = ui.LINE_CAP_ROUND path.move_to(20,20) path.line_to(80,20) path.stroke() image = ctx.get_image() #background_imageは、ボタンの背景画像を入れるuiメソッド self.scv_btn_path_width.background_image = image #線幅のボタンに線色と線幅を反映した画像を背景画像として入れている。 #特に意味は無い。 def layout(self): pass #bivに座標変換したbpathでbivに作画 def path_action(self, sender): #path = sender.path bpath= sender.bpath#pvなどのインスタンス内init外の属性にアクセスする時に必要。関数にもsender要る。 # 今回の場合「インスタンス内init外の属性」=self.pv.bpath.line_width .但し、pvのactionに登録無いとエラー。 old_img = self.biv.image width, height = self.image_w,self.image_h #bivのサイズ取得だと、画像縮小時に画像の左上部分が切り取られる為、使えない。 self.logp(sender,'path_action') #pathをイメージに作画している。w hは、作画範囲なのでbase_imgeのサイズ。zoomされるbivサイズではない。 with ui.ImageContext(width, height) as ctx: if old_img: old_img.draw() self.pv.bpath.line_width = self.path_width#線の幅を反映させる ui.set_color(self.path_color)#pathの作画色を反映させる bpath.stroke() self.biv.image = ctx.get_image() def path_undo(self,sender): #bpath= sender.bpath last_path_color = self.path_color last_path_width = self.path_width #bpathsリストの個数をカウントしている。undoで再度全bpathを再作画する為。 path_count = len(self.pv.bpaths) #print(self.pv.bpaths) width, height = self.image_w,self.image_h #print('w h '+str(width)+' , '+str(height)) if path_count > 0: self.pv.bpaths.pop()#最後の要素を削除する path_count -= 1 self.biv.image = self.base_image # bivの画像をbpath無に戻す。 for i in range(0, path_count):#bpathを最初から1つ前のbpathまで再作画しなおす。 self.path_width = self.pv.bpaths[i][3]#self.path_width#線の幅を反映させる self.path_color = self.pv.bpaths[i][2]# self.pv.bpath = self.pv.bpaths[i][0] self.pv.pv_bpath_undo()#線幅を反映させる為にpv中でbpthを生成させている self.path_color = last_path_color self.path_width = last_path_width self.set_needs_display() def zoom_set(self,sender): scvw,scvh = self.scr_w,self.scr_h-self.btn_h-80#scvのサイズを取得 self.scvoffset_x,self.scvoffset_y = self.scv.content_offset#scvの左上原点のbiv中の位置 #scv offsetを拡大縮小した時の位置近くに移動させる newx=self.scvoffset_x * self.scvrate / self.scvrate0 newy=self.scvoffset_y * self.scvrate / self.scvrate0 self.biv.frame = (0,0,self.image_w*self.scvrate,self.image_h*self.scvrate) self.scv.content_size = (self.image_w*self.scvrate,self.image_h*self.scvrate)#元画像を入れる枠サイズ self.scvoffset_x,self.scvoffset_y = self.scv.content_offset#scvの左上原点のbiv中の位置 self.pv.scvoffset_x = self.scvoffset_x#pvの中に現在のscvoffsetを流し込む。 self.pv.scvoffset_y = self.scvoffset_y self.pv.scvrate = self.scvrate self.scv.content_size = (self.image_w*self.scvrate,self.image_h*self.scvrate)#元画像を入れる枠のサイズ self.scv.content_offset=(newx,newy)#scv offsetを元の位置に近い位置に移動する。左上基準。 self.scvoffset_x,self.scvoffset_y = self.scv.content_offset self.pv.scvoffset_x = self.scvoffset_x#pvの中に現在のscvoffsetを流し込む。 self.pv.scvoffset_y = self.scvoffset_y self.logp(sender,'zoom_set') #titleに画像サイズと倍率を表示 self.name ='Size:' + str(int(self.image_w)) + ', ' + str(int(self.image_h))+' rate:'+str(self.scvrate)+' '+self.filename1 #log print def logp(self,sender,memo): print(str(memo)+' biv.w'+str(self.biv.width)+' h'+str(self.biv.height)+' : offset'+str(self.scv.content_offset)+' rate'+str(self.scvrate)+' rate0:'+str(self.scvrate0)) ############################### #v = epaint() #v.present('fullscreen') if __name__ == "__main__": epaint('epaint')#filename1を引数にしたい。
- 投稿日:2021-01-16T21:08:39+09:00
[Swift] core dataの基本的な実装をまとめてみた
はじめに
core dataについて色々調べ、とりあえず実装できた状態です。
自分なりにまとめてみます。
tableviewcontrollerと、遷移先の項目を追加するview2つを作成します。
core data以外の操作は省略します?♂️実装と解説
1 プロジェクト作成
まずはじめにプロジェクトを立ち上げる際に、User Core Dataを選択します。
すると、ファイル名.xcdatamodeldというファイルが追加されます、2 tableviewcontrollerと追加用のviewcontrollerの実装
Core Dataの設定
①のファイルを選択すると、Core Dataの設定ができます。
②でEntityを追加します。
③が追加されます。Entityは今回データを入れておいて保管する場所になります。
今回はメモ内容の保存に使いますので、Memoという名前にしてみます。
Entityは大文字からの名前をつけるのが無難だそうです。
④の+を押すと、⑤のAttributeがが追加できます。これは今回追加するデータになります。
今回はメモの内容を保存するnameをstring型、チェックマークの状態を保存するcheckをBoolean型で設定します。
型はTypeから設定できます。
各項目のOptionalのチェックを外しておいてください。
ここで一旦ビルドしておきます。しておかないと後々エラーになるそうです。
コード
保存の処理
①
viewContext
は変更などを見にいき、操作もできるメソッドで、それを定数contextに入れる
②そのcontextをMemo(context: context)に入れる事で、Core DataであるMemoの変更や操作ができるようになる。
③それを定数memoに入れたので、memo.attributeの項目を操作できる
④最後にsaveContext()でデータを保存して完了!import UIKit class AddViewController: UIViewController { @IBOutlet weak var addTextField: UITextField! override func viewDidLoad() { super.viewDidLoad() } @IBAction func addButton(_ sender: Any) { // persistentContainerは総監督の役割でcore dataに関連するNSManagedObjectContext・ // NS PesistentStoreCoordinator・NSpersistentStoreに指示を出せる。 // 以下のcontextはManagedObjectContextへの参照が含まれている。 // viewContextはNSManagedObjectContext型で、管理されているオブジェクトを操作したり、 // 変更を追跡したりする。 let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext // (context:)はモデル内の単一のエンティティを表すNSManagedObject(この場合はMemo)のサブクラスに対してのみ、 // 合法的に呼び出すことができる。この場合、Memoの中で変更があるかとかをcontextで追跡し、 // それを呼び出して定数に入れているイメージかな?ManagedObjectのサブクラスを初期化し、 // 指定されたNSManagedObjectContextに挿入できるようになる。 let memo = Memo(context: context) //memoはManagedObjectのサブクラスを初期化し、 //指定されたNSManagedObjectContextに挿入できるようになったので、 //以下のように値などを入れる事ができる。 memo.name = addTextField.text memo.check = false // 変更があれば保存する処理 (UIApplication.shared.delegate as! AppDelegate).saveContext() navigationController!.popViewController(animated: true) } }データを取得して、表示するコード
import UIKit class MemoTableViewController: UITableViewController { // 変数memosを用意して、core dataのMemo型を追加していくためにMemo型に指定する var memos : [Memo] = [] override func viewDidLoad() { super.viewDidLoad() tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: TableViewCell.reuseIdentifier) } override func viewWillAppear(_ animated: Bool) { // データを取得する getData() tableView.reloadData() } // MARK: - Table view data source override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // #warning Incomplete implementation, return the number of rows return memos.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.reuseIdentifier, for: indexPath) as! TableViewCell let memo = memos[indexPath.row] cell.configure(isCheck: memo.check, name: memo.name!) return cell } func getData() { // 変更箇所を見にいく let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext do { // あれば取得 memos = try context.fetch(Memo.fetchRequest()) } catch { print("読み込み失敗") } } }cellの内容
import UIKit class TableViewCell: UITableViewCell { @IBOutlet weak var label: UILabel! @IBOutlet weak var checkImage: UIImageView! static let image = UIImage(named: "check") static let reuseIdentifier = "Cell1" func configure(isCheck: Bool, name: String) { // checkマークの表示の処理 checkImage.image = isCheck ? TableViewCell.image : nil label.text = name } }データの内容を削除
editingStyle内で処理を書きます。
また同じように、viewContextで操作できるようにします。
context.delete
で削除できます。
あとは削除した状態をsaveContext()
で保存してあげれはOKです。override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext if editingStyle == .delete { let memo = memos[indexPath.row] context.delete(memo) (UIApplication.shared.delegate as! AppDelegate).saveContext() do { // あれば取得 memos = try context.fetch(Memo.fetchRequest()) } catch { print("読み込み失敗") } } tableView.reloadData() }checkマークの切り替え
!を先頭につける事で値が反転できます。
これをdidSeleceRowAtに書く事で、cellのタップ時に切り替える事ができます。override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let memo = memos[indexPath.row] memo.check = !memo.check tableView.reloadData() }最後に
まだまだ理解できていないところがありますが、実装段階での理解の手助けになれば幸いです。
間違っている箇所などありましたら遠慮なくご指摘ください。参考サイト
https://blog.codecamp.jp/programming-iphone-app-development-todo
- 投稿日:2021-01-16T17:51:25+09:00
Xcode 12.3 が頻繁にフリーズする問題に対処した
はじめに
数分に1回、Xcode v12.3 がフリーズする状況に陥りました。
フリーズするとレインボーアイコンが出て、数分間操作ができなくなります。
だいぶ辛いです。環境
- macOS Catalina v10.15.7
- Xcode v12.3
対処方法
Apple のDeveloper Forums にXcode freezes after upgrade to 12.3 という質問が挙がっています。その回答を参考にします。
Devices and Simulators を開く
Xcode が応答するまで待つか、Xcode を強制終了させて再度立ち上げます。
Xcode が応答しなくなるまでの間に、メニューでWindow
->Devices and Simulators
を起動させておきます。Connect via network のチェックを外す
Devices and Simulators に登録されている全ての実機について、Connect via network のチェックを外します。
(ワイヤレスデバッグが出来なくなるが致し方ない。。)Unpair Device を行う
Devices and Simulators に登録されているが、電源をオフにしており現在起動していない端末について。
それら全ての実機について、Unpair Device を行って登録を解除します。
(必要があれば、再度デバイスを登録します。)さいごに
筆者の環境では、上記の対応をすることによってXcode のフリーズが起こらなくなりました。
記事執筆時点ではDeveloper Forums のスレッドでは未解決となっています。
次のXcode バージョンで解決されるのでしょうか。
- 投稿日:2021-01-16T17:23:03+09:00
モバイルアプリ開発は、Flutter一択なのか?-2021版-
こんにちは! Tetsukick(菊池哲平)です。
2021年1月現在、インドネシア(PT.AQ Business Consulting Indonesia)でモバイルアプリ開発の技術顧問をしております。
iOS歴5年、Flutter歴1年半。個人でもアプリ開発してます。
本記事は、2020年11月にZennに投稿した記事の改訂版になります。
本記事の対象読者
- モバイルアプリ開発者
- Flutterの今後の可能性を知りたい方
- モバイルアプリ開発案件を検討中で技術選定をされている方
- 本記事のタイトルが気になる方
序
今回インドネシアでモバイル開発の技術顧問をさせていただく中で、Flutterの提案から導入までを実施し、実際に導入に至ることができましたので、その過程で用いた技術的根拠等々をシェアいたします。
実際に非技術者に対しても提案をしましたので、そのままお使いいただくことも可能かと思います。
PPT素材が必要な方いましたらTwitter_@tpi29までDMいただければと思います。
日本語文献や英語文献も参考にしておりますので、少しでもお役に立てばと思います。最近の更新
2021年版のDeveloperのロードマップが公開されました。
結果からお伝えすると、、
モバイル開発におけるフレームワークは、React Nativeが取り上げられております。とはいえ僕の記事を読んでもらえば少し分かるかもしれませんが、現状そこまで大差はないと思っております。結局は好きな言語を身につけるのが一番かと。。笑
ネイティブ開発とクロスプラットフォーム開発
まず、モバイルアプリ開発をするにあたって、大きく2つに分類(ネイティブ開発とクロスプラットフォーム開発)できます。
ネイティブ開発とは
iOSアプリ、Androidアプリに対してそれぞれ独自の言語を用いて開発する開発手法です。
iOSであれば、Objective-CやSwift。
Androidであれば、AndroidJavaやKotlin。
での開発を指します。モバイルアプリ開発における最も基本的な開発手法になります。
クロスプラットフォーム開発
クロスプラットフォーム開発とは、AndroidやiOS等の複数のプラットフォームにまたがって開発を行うことのできる手法になります。
具体的には、XamarinやCordova、ReactNative、Flutter、Outsystems等々、最近では、プログラミングを必要としないノーコード開発ツールもいくつか見受けられるようになりました。ネイティブ開発 VS クロスプラットフォーム開発
では、まずネイティブ開発とクロスプラットフォーム開発の比較をいたします。
以下の項目で比較いたしますが、各項目の重要度は読者様で配分頂いた上で実際に選定していただければ良いかと思います。
- 開発効率
- 保守・運用
- パフォーマンス
- 端末固有機能
- 最新機能対応
- 最新OS対応まとめ
開発効率
開発効率においてはクロスプラットフォーム開発に軍配があがります。
人的リソース
ネイテイブ開発
ネイティブ開発は各OSごとの異なる言語(iOS: Swift, Android: java, Kotlin)のエンジニアが必要(通常最低2名)
クロスプラットフォーム開発
一つの言語で開発可能。
開発環境
クロスプラットフォーム開発の多くのプラットフォームでホットリロード機能(コンパイル不要でソースコードの修正をアプリに反映させる機能)を有している。
通常ネイティブ開発では、コンパイルごとに10秒-1分程度の時間がかかるので、その時間を削減することができる。その他
ネイティブの場合、ソースコードは各OSごとに必要で基本的にロジックをあわせる必要があるが、開発者間での調整コストやソースコード間でずれが生じるリスクが発生する。
これに関してはProtocol Buffers等で極力ロジックを統一する等の対策も可能だが、すべてを補完できるわけではない。保守・運用
保守・運用面では、クロスプラットフォーム開発に軍配があがります。
人的リソース
ネイテイブ開発
ネイティブ開発は各OSごとの異なる言語(iOS: Swift, Android: java, Kotlin)のエンジニアが必要(通常最低2名)
クロスプラットフォーム開発
一つの言語で開発可能。
不具合・改修リスク
ネイティブ開発では各OSごとにソースコードが存在するため、不具合の発生リスクも増加する。
改修の際は両OSでの対応が必要になる。パフォーマンス
レンダリング(画面描画)パフォーマンス
各プラットフォームにより異なるが、クロスプラットフォームはネイティブUIを呼び出す処理を行うもの、WebViewへのレンダリングを行うもの、独自レンダリングエンジンを使用するもの、等ありますが、基本的にはネイティブのレンダリングパフォーマンスに比較すると劣ります。
以下はパフォーマンスの比較例です。
リストを表示する上でのパフォーマンスを比較しています。
端末固有機能
一般的端末固有機能
カメラ機能、プッシュ通知、GPS、生体認証等の機能はプラグインによりサポートされているため、
クロスプラットフォーム開発においても、懸念することはほとんどない。特殊な端末固有機能
ウィジェット機能、特殊端末との接続(クレジット端末など)、フィットネスAPI等の連携、センサー関連の使用(加速度センサー等)に関しては、プラットフォームにより異なるが、プラグインが提供されていなかったり、プラグインが不十分な場合があり、自作等での対応が必要になるケースがある
最新機能・最新OS対応
最新機能・最新OS対応においては、ネイティブ開発に軍配があがる。
ネイテイブ開発
最新OSのbeta版や開発用のIDEのbeta版が提供され、リリース前から検証や開発を実施し、備えることが可能。
クロスプラットフォーム開発
リリース後にオープンソース上の開発者間で対応バージョンが追ってリリースされることになり、対応を待つ必要がある。
iOSでもAndroidでも、最新OSのリリースの約一年後には、最新OSに対応していないアプリは新たに申請できなくなる風潮がある。結論(ネイティブ開発 VS クロスプラットフォーム開発)
アプリの要望や開発予算、人的リソース等の状況により、ネイティブ開発、クロスプラットフォーム開発の選定の大きな要因となる。
アプリの要望
特殊なネイティブ機能や最新機能を使用するアプリでは、ネイティブ開発が好ましい。
一般的な機能にとどまるアプリに関しては、クロスプラットフォーム開発が好ましい。開発予算、人的リソース
開発予算、人的リソースが豊富な場合は、ネイティブ開発が好ましい。
予算や人的リソースに懸念がある場合は、クロスプラットフォーム開発が好ましい。クロスプラットフォーム開発
まとめ
クロスプラットフォーム開発分類
上記のようにクロスプラットフォーム開発は主に3つの種類に分類されます
ネイテイブUI型
単一言語をネイティブのUIに変換して表示、パフォーマンスは比較的に高いが、変換にかかる時間だけパフォーマンスが若干落ちる。各ネイティブのUIを使用できるため、単一言語で各OSに合わせたUIを作成可能
オリジナルUI型
プラットフォーム固有のレンダリングエンジンを用いているため、ゲームアプリなど自由度の高いグラフィック表現が可能。パフォーマンスは各プラットフォーム次第だが、こちらも比較的高い。Unityもこちらに分類される。
WebUI型
WebView上に描画を行うため、パフォーマンスが低い。Webと同じリソースを活用できるため、Webサイトのアプリ化等に優れている。ネイティブの機能との連携もやや困難
ReactNative VS Flutter
本記事では、私自身の経験があり、クロスプラットフォーム開発でもよく比較されるReactNativeとFlutterの比較を行います。
まとめ
以下の項目で比較をいたします。
- 言語
- 生産性
- UI自由度
- 開発頻度
言語
言語に関して、ReactNativeで採用されているJavascriptの方が開発者数や情報量などで大いに優勢。
下記のサイトでは、かなりの大差ですね。
ランキング 言語 Share率 3位 Javascript 8.44% 21位 Dart 0.56% 生産性
生産性に関しては、ホットリロード機能はどちらもサポートされているが、インタフェースの作成において、XDから正式に変換プラグインのサポートやアニメーションフォーマットのサポート(RIVE)などインタフェースの開発効率が高い
UI自由度
UI自由度に関しては、ネイティブコンポーネントだけでなく、独自のUIコンポーネントライブラリを保有するFlutterの方が優れている。
開発頻度
OSSとして、言語の改良速度がFlutterが非常に活発な状態にあります。
質問投稿サイト(StackOverFlow)の質問数やGoogle検索の数に関してもFlutterが優れている。
github_flutter
github_reactnative結論(クロスプラットフォーム開発比較)
プラットフォームの選定には、開発者の習熟言語、アプリの要望にも起因する。
アプリの要望
既にWebサイト構築済みで、特異な機能のないアプリ開発であれば、Cordova等のWebリソースを使用できる開発が好ましい。
OSごとのネイティブUIの表現を重視する場合は、ReactNativeでの開発が好ましい。
ユーザの要望などUIの変更への対応、開発速度が求められる場合はFlutterでの開発が好ましい。個人的所見
ここ最近の動向を見ても、ReactNativeは、Facebookのネイティブ化などの流れがあることに反して、Flutterは、各種GoogleのアプリがFlutterに変更されたり、中国系のAlibabaやBaidu,TencentなどがFlutterでの開発をしていたり(Flutter showcase)、そういった面でも信頼ができるのではと思います。
また実際に、私がインドネシアで比較的容易に導入できた背景としては、約9割のスマホユーザがAndroidユーザである背景から、ユーザに広く普及しているUIがMaterial Designであるため、その開発においての優位性があるなども別途決め手の一つにはなっていたかと思います。
参考記事
- 投稿日:2021-01-16T16:57:22+09:00
UITextFiledで一発でplaceHolderの色を選択できるExtensionを考えてみた
はじめに
どうもこんにちは、TOSHです。
UITextFieldを使用したときに、textColorはそのままセットできるのに、なぜか、placeHolderの色はそのままセットできないって感じた人ことある人は多いと思います。
ということで、placeHolderについてもTextColorと同じようにセットできるようなExtensionを考えてみました。通常の方法
通常、placeHolderの色を変更する場合は、
let color: UIColor = ~ここで任意の色を指定する~ textField.attributedPlaceholder = NSAttributedString( string: "placeHolderの中の文字", attributes: [NSAttributedString.Key.foregroundColor : color])まあ、別にそんなに大変ではないが、NSAttibutedStringを使うのかといった感じですよね。
Extensionを用いた実装方法
extension UITextField { var placeHolderColor: UIColor { set { self.attributedPlaceholder = NSAttributedString( string: self.placeholder ?? "", attributes: [NSAttributedString.Key.foregroundColor : newValue]) } get { let defaultPlaceHolderGray = UIColor(red: 0, green: 0, blue: 0.0980392, alpha: 0.22) guard self.attributedPlaceholder?.length != 0, let placeHolderColor = self.attributedPlaceholder?.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? UIColor else { return defaultPlaceHolderGray } return placeHolderColor } } }extensionのなかで、Stored Propertyはできないので、Computed Propertyを使用します。
setterは簡単なのですが、意外と苦戦したのは、getter。
Place Holderのなかに文字が入っていない可能性もあるので、self.attributedPlaceholder?.length != 0でガードをする必要があります。あと、あくまで、attributedPlaceholderの1文字目を取っているだけなので、1文字目以降で文字が変わるようなAttributedString出会った場合は、うまく動いていないと思っていいでしょう。
ただ、セットすることはあってもなかなか取ってくることはないと思われるのでこれについてはあまり深く考えなくてもいい気がします。。。まとめ
実装していて気づいたことは、そもそも、Place Holderの色をセットしたが、中身をセットしないということはないので、textのセットと同時に行うことのできるNSAttributedStringを使用しているののだなと思いました。(Textの場合は、ユーザーが文字を入力するので、初期状態で、色を指定しておいて、Textを指定しないということがメインの使い方)
なので、せっかく作ったはいいんですけど、あまり使用されないようなExtensionだなーという感覚です。
- 投稿日:2021-01-16T15:56:27+09:00
メモ イヤホン抜き差し
iOSでAudioKitを利用して音を入力する機能を実装する際、本体とイヤホンのマイクを考慮する必要がある。
本体とマイクとでは、サンプルレートが異なることがあり、本体は44.1kHz、iPhone同梱のLightningイヤホンは48kHzとなる。
抜き差しは、AVAudioSessionRouteChangeNotification
を利用するのではなく、AVAudioEngineConfigurationChangeNotification
をトリガーにして、AKMicrophone
を再インスタンス化しつなげ直し、AVAudioEngine
を再開させる(AKManager.engine.start()
)。
また、AudioKit
がAVAudioEngineConfigurationChangeNotification
をハンドリングしているので、事前に無効にしておく (AKSettings.enableCategoryChangeHandling = false
)。
- 投稿日:2021-01-16T13:10:06+09:00
【SwiftUI】Mapビューを使用する
この記事は何?
iOS13では、SwiftUIでMapKitを扱うにはUIKit互換のビューでラップする必要がありました。iOS14で、SwiftUIネイティブなビューとしてMapが実装されたようです。
ここでは、「アップル本社の場所を地図上に表示するビュー」のコードを載せておきました。環境
- macOS11.1
- Xcode12.3
- Swift5.3
コード
SwiftUIのMapビューimport SwiftUI import MapKit struct MapView: View { @State var region = MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 37.3351, longitude: -122.0088), span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)) var body: some View { Map(coordinateRegion: $region) } } struct MapView_Previews: PreviewProvider { static var previews: some View { MapView() } }プレビュー
- 投稿日:2021-01-16T10:29:51+09:00
【iOS】Firebaseど素人のFirebase入門 認証編
Firebaseど素人だったので、Firebase学習の為に公式ドキュメントを見て、学びをサンプルアプリという形でアウトプットしました。
まずはAuthentication
(認証)に挑戦!作ったサンプルアプリのデモ
現在使用中のユーザーメールアドレスがラベルに表示されるアプリです。
・メールアドレスとパスワードを使った新規登録(サインアップ)
・登録済みのアカウントの認証(サインイン)
・サインアウト
・匿名認証この機能の実装を行いました。
まずはFirebaseに登録
Firebase公式ドキュメントに沿って、Firebase登録とFirebase SDKをアプリに追加する。
Firebase を iOS プロジェクトに追加するアプリ内でFirebaseの初期化を行う
AppDelegate.swiftにFirebaseをインポート
AppDelegate.swiftimport Firebase
didFinishLaunchingWithOptions
内でFirebaseApp.configure()
を宣言AppDelegate.swiftfunc application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { FirebaseApp.configure() return true }使用するログインプロバイダのステータスを変更する
今回はメール/パスワード認証と匿名認証をログインに使用するので、Firebaseに登録したアプリのプロジェクト内
Authentication
のSign-in method
で
メール/パスワード
匿名
こちらの二つのステータスを有効に変更します。
ViewControllerにFirebaseをインポート
作成を進めるファイルにもFirebaseをインポートする。
AuthViewController.swiftimport Firebaseメール/パスワード認証
新規登録
新規登録には
createUser(withEmail:, password:, completion:)
を使用します。Auth.auth().createUser(withEmail: email, password: password) { (authResult, error) in guard let user = authResult?.user, error == nil else { print("登録に失敗しました:" ,error!.localizedDescription) return } print("登録に成功しました", user.email!) }サインイン
サインインには、
signIn(withEmail:, password:, completion:)
を使用します。Auth.auth().signIn(withEmail: email, password: password) { (authResult, error) in guard let user = authResult?.user, error == nil else { print("サインインに失敗しました:" ,error!.localizedDescription) return } print("サインインに成功しました", user.email!) }匿名ログイン
匿名ログインには
Auth.auth().signInAnonymously(completion:)
を使います。Auth.auth().signInAnonymously { (authResult, error) in guard let user = authResult?.user, error == nil else { print("匿名サインインに失敗しました:" ,error!.localizedDescription) return } print("匿名サインインに成功しました", user.uid) }サインアウト
signOut()
を呼ぶだけです。do { try Auth.auth().signOut() } catch let signOutError as NSError { print("サインアウトに失敗しました:", signOutError) }現在ログインしているユーザーを取得する
現在ログインしているユーザーを取得するには、Auth オブジェクトでリスナーを設定することをおすすめします。
とのことで、リスナーを設定してみました。
1.
AuthStateDidChangeListenerHandle
をグローバル変数として定義AuthViewController.swiftvar authHandle: AuthStateDidChangeListenerHandle?2.
viewWillAppear
でaddStateDidChangeListener(listener:)
をアタッチoverride func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) authHandle = Auth.auth().addStateDidChangeListener({ (auth, user) in //ログイン状態が変更された時の処理を書く }) }
viewWillAppear
でアタッチしたこのリスナーは、ユーザーのログイン状態が変更される度に呼び出されます。
変更される度に実行したい処理をここに書きます。
とても便利ですね。
ログインされてるなら別ページに遷移、ログイン無しなら登録ページを表示などの使い分けにも有効そうです。その他の方法では、
currentUser
でログインユーザーの状態も取得出来るようです。currentUser を使用することでも、現在ログインしているユーザーを取得できます。ユーザーがログインしていない場合、currentUser は nil です。
3.
viewWillDisappear
でリスナーを切り離します。override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) Auth.auth().removeStateDidChangeListener(authHandle!) }まとめ
Firebaseの公式ドキュメントはとても充実しており、日本語ページもあるので簡単にここまでは進めれました。
もっとしっかり公式ドキュメントを読み、理解を深めていきたいと思います。
何か間違いがありましたら、優しく指摘していただけると幸いです?♂️参考
iOS で Firebase Authentication を使ってみる
サンプリアプリのコード全体
import UIKit import Firebase class AuthViewController: UIViewController { @IBOutlet weak var emailTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! @IBOutlet weak var currentUserEmailLabel: UILabel! @IBOutlet weak var signUpButton: UIButton! @IBOutlet weak var signInButton: UIButton! var authHandle: AuthStateDidChangeListenerHandle? override func viewDidLoad() { super.viewDidLoad() signUpButton.layer.cornerRadius = 22 signInButton.layer.cornerRadius = 22 emailTextField.delegate = self passwordTextField.delegate = self } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) authHandle = Auth.auth().addStateDidChangeListener({ (auth, user) in if let currentUser = user { //もし、ユーザーが匿名で利用していたら if currentUser.isAnonymous { self.currentUserEmailLabel.text = "匿名で利用中" } else { self.currentUserEmailLabel.text = currentUser.email } } else { //ログインをしていない場合 self.currentUserEmailLabel.text = "現在ログイン中のユーザーはいません" } }) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) Auth.auth().removeStateDidChangeListener(authHandle!) } // MARK: - Button Actions //サインアウト @IBAction func didTapSignOut(_ sender: UIBarButtonItem) { do { try Auth.auth().signOut() } catch let signOutError as NSError { print("サインアウトに失敗しました:", signOutError) } } //サインアップ @IBAction func didTapSignUp(_ sender: UIButton) { if let email = emailTextField.text, let password = passwordTextField.text { Auth.auth().createUser(withEmail: email, password: password) { (authResult, error) in guard let user = authResult?.user, error == nil else { print("登録に失敗しました:" ,error!.localizedDescription) return } print("登録に成功しました", user.email!) } } } //サインイン @IBAction func didTapSignIn(_ sender: UIButton) { if let email = emailTextField.text, let password = passwordTextField.text { Auth.auth().signIn(withEmail: email, password: password) { (authResult, error) in guard let user = authResult?.user, error == nil else { print("サインインに失敗しました:" ,error!.localizedDescription) return } print("サインインに成功しました", user.email!) } } } //匿名サインイン @IBAction func didTapSignInAnonymously(_ sender: UIButton) { Auth.auth().signInAnonymously { (authResult, error) in guard let user = authResult?.user, error == nil else { print("匿名サインインに失敗しました:" ,error!.localizedDescription) return } print("匿名サインインに成功しました", user.uid) } } } // MARK: - TextField Delegate Methods extension LoginViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } }
- 投稿日:2021-01-16T01:40:51+09:00
JUCE: AirDropでファイルを受けられるようにする
問題
JUCEにはContentSharerというクラスがあり、テキストやファイルを他のデバイスにシェアできる機能があるのだが、受けることができない。これはアホなのでなんとかしたい。目標はAirDropでファイルを送って受けられるようにすること。送る側はContentSharerを使うだけなので割愛。
方法
JUCEのソースをいじらないと流石にできなかった。iOS/Macのコードに手を入れる。このメソッドを追加。ring2 changeで括ってあるところが変更点。自信がないのでまどろっこしく書いているがもっと短く書いて良いと思う。
modules/juce_gui_basics/native/juce_ios_Windowing.mm#if defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - (void) userNotificationCenter: (UNUserNotificationCenter*) center willPresentNotification: (UNNotification*) notification withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler; - (void) userNotificationCenter: (UNUserNotificationCenter*) center didReceiveNotificationResponse: (UNNotificationResponse*) response withCompletionHandler: (void(^)())completionHandler; // ring2 change--> - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options; // <-- ring2 change #endif #endif @end @implementation JuceAppStartupDelegate NSObject* _pushNotificationsDelegate; // ring2 change--> - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options { auto* juceApp = JUCEApplicationBase::getInstance(); NSString* absoluteString = [url absoluteString]; const char* cstr = [absoluteString UTF8String]; URL juceUrl(cstr); BOOL result = juceApp->openUrl(juceUrl); return result; } // <-- ring2 change - (id) init { self = [super init]; appSuspendTask = UIBackgroundTaskInvalid; #if JUCE_PUSH_NOTIFICATIONS && defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0次に、ApplicationBaseにメソッド1つ追加。
modules/juce_events/messages/juce_ApplicationBase.h/** This method is called when the application is being woken from background mode by the operating system. */ virtual void resumed() = 0; // ring2 change--> virtual bool openUrl(URL& url) { return false; } // <--ring2 change /** If any unhandled exceptions make it through to the message dispatch loop, this callback will be triggered, in case you want to log them or do some other type of error-handling. If the type of exception is derived from the std::exception class, the pointer passed-in will be valid. If the exception is of unknown type, this pointer will be null. */ virtual void unhandledException (const std::exception*, const String& sourceFilename, int lineNumber) = 0;Projucerを使っている場合は、プロジェクトの設定でPreprocessor DefinitionsにJUCE_USE_CUSTOM_PLUGIN_STANDALONE_APP=1を書いて、modules/juce_audio_plugin_client/Standalone/juce_StandaloneFilterApp.cppをコピーしたMyStandAloneFilterApp.cppのようなものをプロジェクトに追加し、openUrl()をオーバーライドすれば良い。プラグインのスタンドアロン版の場合はこんなふうにすればOK。自分のプラグインのクラスのヘッダをincludeして。
Source/MyStandAloneApp.cppbool openUrl(URL& url) override { StandalonePluginHolder* holder = StandalonePluginHolder::getInstance(); jassert(holder != nullptr); IfwAudioProcessor* processor = (IfwAudioProcessor*)holder->processor.get(); jassert(processor != nullptr); processor->openUrl(url); return false; }Xcodeでファイルタイプの設定が必要なので、ProjucerのiOSのExporterでCustom Plistに下記のように書いておけば、Info.plistに書き込まれる。UTExportedTypeDeclarationsが適切に記述されていないとopenUrlが呼ばれない模様。記述が足りなかったりすると、iOS 12では呼ばれるがiOS 13以降では呼ばれないとかいうことがあった。
<plist> <key>UTExportedTypeDeclarations</key> <array> <dict> <key>UTTypeConformsTo</key> <array> <string>public.data</string> <-- public.xmlとかpublic.textとかデータに合わせて --> </array> <key>UTTypeDescription</key> <string>My Data</string> <key>UTTypeIconFile</key> <string>Icon.icns</string> <key>UTTypeIconFiles</key> <array/> <key>UTTypeIdentifier</key> <string>com.myurl.myapp.mydata</string> <key>UTTypeTagSpecification</key> <dict> <key>public.filename-extension</key> <array> <string>mydata</string> </array> </dict> </dict> </array> <key>CFBundleDocumentTypes</key> <array> <dict> <key>CFBundleTypeExtensions</key> <array> <string>mydata</string> </array> <key>CFBundleTypeName</key> <string>My App</string> <key>CFBundleTypeRole</key> <string>Editor</string> <key>LSItemContentTypes</key> <array> <string>com.myurl.myapp.mydata</string> </array> <key>LSTypeIsPackage</key> <false/> <key>NSPersistentStoreTypeKey</key> <string>Binary</string> </dict> </array> <key>LSSupportsOpeningDocumentsInPlace</key> <true/> </dict> </plist>