Flutter Boost 混合開(kāi)發(fā)框架
一、Flutter Boost簡(jiǎn)介
眾所周知,F(xiàn)lutter是一個(gè)由C++實(shí)現(xiàn)的Flutter Engine和由Dart實(shí)現(xiàn)的Framework組成的跨平臺(tái)技術(shù)框架。其中,F(xiàn)lutter Engine負(fù)責(zé)線程管理、Dart VM狀態(tài)管理以及Dart代碼加載等工作,而Dart代碼所實(shí)現(xiàn)的Framework則負(fù)責(zé)上層業(yè)務(wù)開(kāi)發(fā),如Flutter提供的組件等概念就是Framework的范疇。
隨著Flutter的發(fā)展,國(guó)內(nèi)越來(lái)越多的App開(kāi)始接入Flutter。為了降低風(fēng)險(xiǎn),大部分App采用漸進(jìn)式方式引入Flutter,在App里選幾個(gè)頁(yè)面用Flutter來(lái)編寫(xiě),但都碰到了相同的問(wèn)題,在原生頁(yè)面和Flutter頁(yè)面共存的情況下,如何管理路由,以及原生頁(yè)面與Flutter頁(yè)面之間的切換和通信都是混合開(kāi)發(fā)中需要解決的問(wèn)題。然而,官方?jīng)]有提供明確的解決方案,只是在混合開(kāi)發(fā)時(shí),官方建議開(kāi)發(fā)者,應(yīng)該使用同一個(gè)引擎支持多窗口繪制的能力,至少在邏輯上做到FlutterViewController是共享同一個(gè)引擎里面的資源。換句話說(shuō),官方希望所有的繪制窗口共享同一個(gè)主Isolate,而不是出現(xiàn)多個(gè)主Isolate的情況。不過(guò),對(duì)于現(xiàn)在已經(jīng)出現(xiàn)的多引擎模式問(wèn)題,F(xiàn)lutter官方也沒(méi)有提供好的解決方案。除了內(nèi)存消耗嚴(yán)重外,多引擎模式還會(huì)帶來(lái)如下一些問(wèn)題。
- 冗余資源問(wèn)題。多引擎模式下每個(gè)引擎的Isolate是相互獨(dú)立的,雖然在邏輯上這并沒(méi)有什么壞處,但是每個(gè)引擎底層都維護(hù)了一套圖片緩存等比較消耗內(nèi)存的對(duì)象,因此設(shè)備的內(nèi)存消耗是非常嚴(yán)重的。
- 插件注冊(cè)問(wèn)題。在Flutter插件中,消息傳遞需要依賴(lài)Messenger,而Messenger是由FlutterViewController去實(shí)現(xiàn)的。如果一個(gè)應(yīng)用中同時(shí)存在多個(gè)FlutterViewController,那么插件的注冊(cè)和通信將會(huì)變得混亂且難以維護(hù)。
- Flutter組件和原生頁(yè)面的差異化問(wèn)題。通常,F(xiàn)lutter頁(yè)面是由組件構(gòu)成的,原生頁(yè)面則是由ViewController或者Activity構(gòu)成的。邏輯上來(lái)說(shuō),我們希望消除Flutter頁(yè)面與原生頁(yè)面的差異,否則在進(jìn)行頁(yè)面埋點(diǎn)和其它一些操作時(shí)增加一些額外的工作量。
- 增加頁(yè)面通信的復(fù)雜度。如果所有的Dart代碼都運(yùn)行在同一個(gè)引擎實(shí)例中,那么它們會(huì)共享同一個(gè)Isolate,可以用統(tǒng)一的框架完成組件之間的通信,但是如果存在多個(gè)引擎實(shí)例會(huì)讓Isolate的管理變得更加復(fù)雜。
如果不解決多引擎問(wèn)題,那么混合項(xiàng)目的導(dǎo)航棧如下圖所示。
目前,對(duì)于原生工程混編Flutter工程出現(xiàn)的多引擎模式問(wèn)題,國(guó)內(nèi)主要有兩種解決方案,一種是字節(jié)跳動(dòng)的修改Flutter Engine源碼方案,另一種是閑魚(yú)開(kāi)源的FlutterBoost。由于字節(jié)跳動(dòng)的混合開(kāi)發(fā)的方案沒(méi)有開(kāi)源,所以現(xiàn)在能使用的就剩下FlutterBoost方案。
FlutterBoost是閑魚(yú)技術(shù)團(tuán)隊(duì)開(kāi)發(fā)的一個(gè)可復(fù)用頁(yè)面的插件,旨在把Flutter容器做成類(lèi)似于瀏覽器的加載方案。為此,閑魚(yú)技術(shù)團(tuán)隊(duì)為希望FlutterBoost能完成如下的基本功能:
- 可復(fù)用的通用型混合開(kāi)發(fā)方案。
- 支持更加復(fù)雜的混合模式,比如支持Tab切換的場(chǎng)景。
- 無(wú)侵入性方案,使用時(shí)不再依賴(lài)修改Flutter的方案。
- 支持對(duì)頁(yè)面生命周期進(jìn)行統(tǒng)一的管理。
- 具有統(tǒng)一明確的設(shè)計(jì)概念。
并且,最近Flutter Boost升級(jí)了3.0版本,并帶來(lái)了如下的一些更新:
- 不侵入引擎,兼容Flutter的各種版本,F(xiàn)lutter sdk的升級(jí)不需要再升級(jí)FlutterBoost,極大降低升級(jí)成本。
- 不區(qū)分Androidx和Support分支。
- 簡(jiǎn)化架構(gòu)和接口,和FlutterBoost2.0比,代碼減少了一半。
- 雙端統(tǒng)一,包括接口和設(shè)計(jì)上的統(tǒng)一。
- 支持打開(kāi)Flutter頁(yè)面,不再打開(kāi)容器場(chǎng)景。
- 頁(yè)面生命周期變化通知更方便業(yè)務(wù)使用。
- 解決了2.0中的遺留問(wèn)題,例如,F(xiàn)ragment接入困難、頁(yè)面關(guān)閉后不能傳遞數(shù)據(jù)、dispose不執(zhí)行,內(nèi)存占用過(guò)高等。
二、Flutter Boost集成
在原生項(xiàng)目中集成Flutter Boost只需要將Flutter Boost看成是一個(gè)插件工程即可。和其他Flutter插件的集成方式一樣,使用FlutterBoost之前需要先添加依賴(lài)。使用Android Studio打開(kāi)混合工程的Flutter工程,在pubspec.yaml中添加FlutterBoost依賴(lài)插件,如下所示。
flutter_boost: git: url: 'https://github.com/alibaba/flutter_boost.git' ref: 'v3.0-hotfixes'
需要說(shuō)明的是,此處的所依賴(lài)的FlutterBoost的版本與Flutter的版本是對(duì)應(yīng)的,如果不對(duì)應(yīng)使用過(guò)程中會(huì)出現(xiàn)版本不匹配的錯(cuò)誤。然后,使用flutter packages get命令將FlutterBoost插件拉取到本地。
2.1 Android集成
使用Android Studio打開(kāi)新建的原生Android工程,在原生Android工程的settings.gradle文件中添加如下代碼。
setBinding(new Binding([gradle: this])) evaluate(new File( settingsDir.parentFile, 'flutter_library/.android/include_flutter.groovy'))
然后,打開(kāi)原生Android工程app目錄下的build.gradle文件,繼續(xù)添加如下依賴(lài)腳本。
dependencies { implementation project(':flutter_boost') implementation project(':flutter') }
重新編譯構(gòu)建原生Android工程,如果沒(méi)有任何錯(cuò)誤則說(shuō)明Android成功了集成FlutterBoost。使用Flutter Boost 之前,需要先執(zhí)行初始化。打開(kāi)原生Android工程,新建一個(gè)繼承FlutterApplication的Application,然后在onCreate()方法中初始化FlutterBoost,代碼如下。
public class MyApplication extends FlutterApplication { @Override public void onCreate() { super.onCreate(); FlutterBoost.instance().setup(this, new FlutterBoostDelegate() { @Override public void pushNativeRoute(String pageName, HashMap<String, String> arguments) { Intent intent = new Intent(FlutterBoost.instance().currentActivity(), NativePageActivity.class); FlutterBoost.instance().currentActivity().startActivity(intent); } @Override public void pushFlutterRoute(String pageName, HashMap<String, String> arguments) { Intent intent = new FlutterBoostActivity.CachedEngineIntentBuilder(FlutterBoostActivity.class, FlutterBoost.ENGINE_ID) .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.opaque) .destroyEngineWithActivity(false) .url(pageName) .urlParams(arguments) .build(FlutterBoost.instance().currentActivity()); FlutterBoost.instance().currentActivity().startActivity(intent); } },engine->{ engine.getPlugins(); } ); } }
然后,打開(kāi)原生Android工程下的AndroidManifest.xml文件,將Application替換成自定義的MyApplication,如下所示。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.idlefish.flutterboost.example"> <application android:name="com.idlefish.flutterboost.example.MyApplication" android:label="flutter_boost_example" android:icon="@mipmap/ic_launcher"> <activity android:name="com.idlefish.flutterboost.containers.FlutterBoostActivity" android:theme="@style/Theme.AppCompat" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" > <meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background"/> </activity> <meta-data android:name="flutterEmbedding" android:value="2"> </meta-data> </application> </manifest>
由于Flutter Boost 是以插件的方式集成到原生Android項(xiàng)目的,所以我們可以在Native 打開(kāi)和關(guān)閉Flutter模塊的頁(yè)面。
FlutterBoost.instance().open("flutterPage",params); FlutterBoost.instance().close("uniqueId");
而Flutter Dart的使用如下。首先,我們可以在main.dart文件的程序入口main()方法中進(jìn)行初始化。
void main() { runApp(MyApp()); } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { static Map<String, FlutterBoostRouteFactory> routerMap = { '/': (settings, uniqueId) { return PageRouteBuilder<dynamic>( settings: settings, pageBuilder: (_, __, ___) => Container()); }, 'embedded': (settings, uniqueId) { return PageRouteBuilder<dynamic>( settings: settings, pageBuilder: (_, __, ___) => EmbeddedFirstRouteWidget()); }, 'presentFlutterPage': (settings, uniqueId) { return PageRouteBuilder<dynamic>( settings: settings, pageBuilder: (_, __, ___) => FlutterRouteWidget( params: settings.arguments, uniqueId: uniqueId, )); }}; Route<dynamic> routeFactory(RouteSettings settings, String uniqueId) { FlutterBoostRouteFactory func =routerMap[settings.name]; if (func == null) { return null; } return func(settings, uniqueId); } @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return FlutterBoostApp( routeFactory ); }
當(dāng)然,還可以監(jiān)聽(tīng)頁(yè)面的生命周期,如下所示。
class SimpleWidget extends StatefulWidget { final Map params; final String messages; final String uniqueId; const SimpleWidget(this.uniqueId, this.params, this.messages); @override _SimpleWidgetState createState() => _SimpleWidgetState(); } class _SimpleWidgetState extends State<SimpleWidget> with PageVisibilityObserver { static const String _kTag = 'xlog'; @override void didChangeDependencies() { super.didChangeDependencies(); print('$_kTag#didChangeDependencies, ${widget.uniqueId}, $this'); } @override void initState() { super.initState(); PageVisibilityBinding.instance.addObserver(this, ModalRoute.of(context)); print('$_kTag#initState, ${widget.uniqueId}, $this'); } @override void dispose() { PageVisibilityBinding.instance.removeObserver(this); print('$_kTag#dispose, ${widget.uniqueId}, $this'); super.dispose(); } @override void onForeground() { print('$_kTag#onForeground, ${widget.uniqueId}, $this'); } @override void onBackground() { print('$_kTag#onBackground, ${widget.uniqueId}, $this'); } @override void onAppear(ChangeReason reason) { print('$_kTag#onAppear, ${widget.uniqueId}, $reason, $this'); } void onDisappear(ChangeReason reason) { print('$_kTag#onDisappear, ${widget.uniqueId}, $reason, $this'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('tab_example'), ), body: SingleChildScrollView( physics: BouncingScrollPhysics(), child: Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( margin: const EdgeInsets.only(top: 80.0), child: Text( widget.messages, style: TextStyle(fontSize: 28.0, color: Colors.blue), ), alignment: AlignmentDirectional.center, ), Container( margin: const EdgeInsets.only(top: 32.0), child: Text( widget.uniqueId, style: TextStyle(fontSize: 22.0, color: Colors.red), ), alignment: AlignmentDirectional.center, ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(30.0), color: Colors.yellow, child: Text( 'open flutter page', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => BoostNavigator.of().push("flutterPage", arguments: <String, String>{'from': widget.uniqueId}), ) Container( height: 300, width: 200, child: Text( '', style: TextStyle(fontSize: 22.0, color: Colors.black), ), ) ], ))), ); } }
然后,運(yùn)行項(xiàng)目,就可以從原生頁(yè)面跳轉(zhuǎn)到Flutter頁(yè)面,如下圖所示效果。
2.2 iOS集成
和Android的集成步驟一樣,使用Xcode打開(kāi)原生iOS工程,然后在iOS的AppDelegate文件中初始化Flutter Boost ,如下所示。
@interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { MyFlutterBoostDelegate* delegate=[[MyFlutterBoostDelegate alloc ] init]; [[FlutterBoost instance] setup:application delegate:delegate callback:^(FlutterEngine *engine) { } ]; return YES; } @end
下面是自定義的FlutterBoostDelegate的代碼,如下所示。
@interface MyFlutterBoostDelegate : NSObject<FlutterBoostDelegate> @property (nonatomic,strong) UINavigationController *navigationController; @end @implementation MyFlutterBoostDelegate - (void) pushNativeRoute:(FBCommonParams*) params{ BOOL animated = [params.arguments[@"animated"] boolValue]; BOOL present= [params.arguments[@"present"] boolValue]; UIViewControllerDemo *nvc = [[UIViewControllerDemo alloc] initWithNibName:@"UIViewControllerDemo" bundle:[NSBundle mainBundle]]; if(present){ [self.navigationController presentViewController:nvc animated:animated completion:^{ }]; }else{ [self.navigationController pushViewController:nvc animated:animated]; } } - (void) pushFlutterRoute:(FBCommonParams*)params { FlutterEngine* engine = [[FlutterBoost instance ] getEngine]; engine.viewController = nil; FBFlutterViewContainer *vc = FBFlutterViewContainer.new ; [vc setName:params.pageName params:params.arguments]; BOOL animated = [params.arguments[@"animated"] boolValue]; BOOL present= [params.arguments[@"present"] boolValue]; if(present){ [self.navigationController presentViewController:vc animated:animated completion:^{ }]; }else{ [self.navigationController pushViewController:vc animated:animated]; } } - (void) popRoute:(FBCommonParams*)params result:(NSDictionary *)result{ FBFlutterViewContainer *vc = (id)self.navigationController.presentedViewController; if([vc isKindOfClass:FBFlutterViewContainer.class] && [vc.uniqueIDString isEqual: params.uniqueId]){ [vc dismissViewControllerAnimated:YES completion:^{}]; }else{ [self.navigationController popViewControllerAnimated:YES]; } } @end
如果要在原生iOS代碼中打開(kāi)或關(guān)閉Flutter頁(yè)面,可以使用下面的方式。
[[FlutterBoost instance] open:@"flutterPage" arguments:@{@"animated":@(YES)} ]; [[FlutterBoost instance] open:@"secondStateful" arguments:@{@"present":@(YES)}];
三、Flutter Boost架構(gòu)
對(duì)于混合工程來(lái)說(shuō),原生端和Flutter端對(duì)于頁(yè)面的定義是不一樣的。對(duì)于原生端而言,頁(yè)面通常指的是一個(gè)ViewController或者Activity,而對(duì)于Flutter來(lái)說(shuō),頁(yè)面通常指的是Flutter組件。FlutterBoost框架所要做的就是統(tǒng)一混合工程中頁(yè)面的概念,或者說(shuō)弱化Flutter組件對(duì)應(yīng)容器頁(yè)面的概念。換句話說(shuō),當(dāng)有一個(gè)原生頁(yè)面存在的時(shí)候,F(xiàn)lutteBoost就能保證一定有一個(gè)對(duì)應(yīng)的Flutter的容器頁(yè)面存在。
FlutterBoost框架其實(shí)就是由原生容器通過(guò)消息驅(qū)動(dòng)Flutter頁(yè)面容器,從而達(dá)到原生容器與Flutter容器同步的目的,而Flutter渲染的內(nèi)容是由原生容器去驅(qū)動(dòng)的,下面是Flutter Boost 給的一個(gè)Flutter Boost 的架構(gòu)示意圖。
可以看到,F(xiàn)lutter Boost插件分為平臺(tái)和Dart兩端,中間通過(guò)Message Channel連接。平臺(tái)側(cè)提供了Flutter引擎的配置和管理、Native容器的創(chuàng)建/銷(xiāo)毀、頁(yè)面可見(jiàn)性變化通知,以及Flutter頁(yè)面的打開(kāi)/關(guān)閉接口等。而Dart側(cè)除了提供類(lèi)似原生Navigator的頁(yè)面導(dǎo)航接口的能力外,還負(fù)責(zé)Flutter頁(yè)面的路由管理。
總的來(lái)說(shuō),正是基于共享同一個(gè)引擎的方案,使得FlutterBoost框架有效的解決了多引擎的問(wèn)題。簡(jiǎn)單來(lái)說(shuō),F(xiàn)lutterBoost在Dart端引入了容器的概念,當(dāng)存在多個(gè)Flutter頁(yè)面時(shí),F(xiàn)lutterBoost不需要再用棧的結(jié)構(gòu)去維護(hù)現(xiàn)有頁(yè)面,而是使用扁平化鍵值對(duì)映射的形式去維護(hù)當(dāng)前所有的頁(yè)面,并且每個(gè)頁(yè)面擁有一個(gè)唯一的id
四、FlutterBoost3.0更新
4.1 不入侵引擎
為了解決官方引擎復(fù)用引起的問(wèn)題,F(xiàn)lutterBoost2.0拷貝了Flutter引擎Embedding層的一些代碼進(jìn)行改造,這使得后期的升級(jí)成本極高。而FlutterBoost3.0采用繼承的方式擴(kuò)展FlutterActivity/FlutterFragment等組件的能力,并且通過(guò)在適當(dāng)時(shí)機(jī)給Dart側(cè)發(fā)送appIsResumed消息解決引擎復(fù)用時(shí)生命周期事件錯(cuò)亂導(dǎo)致的頁(yè)面卡死問(wèn)題,并且,F(xiàn)lutterBoost 3.0 也兼容最新的官方發(fā)布的 Flutter 2.0。
4.2 不區(qū)分Androidx和Support分支
FlutterBoost2.0通過(guò)自己實(shí)現(xiàn)FlutterActivityAndFragmentDelegate.Host接口來(lái)擴(kuò)展FlutterActivity和FlutterFragment的能力,而getLifecycle是必須實(shí)現(xiàn)的接口,這就導(dǎo)致對(duì)androidx的依賴(lài)。這也是為什么FlutterBoostView的實(shí)現(xiàn)沒(méi)有被放入FlutterBoost3.0插件中的原因。而FlutterBoost3.0通過(guò)繼承的方式擴(kuò)展FlutterActivity/FlutterFragment的能力的額外收益就是,可以做到不依賴(lài)androidx。
4.3 雙端設(shè)計(jì)統(tǒng)一,接口統(tǒng)一
很多Flutter開(kāi)發(fā)者只會(huì)一端,只會(huì)Android 或者只會(huì)IOS,但他需要接入雙端,所以雙端統(tǒng)一能降低他的 學(xué)習(xí)成本和接入成本。FlutterBoost3.0,在設(shè)計(jì)上 Android和IOS都做了對(duì)齊,特別接口上做到了參數(shù)級(jí)的對(duì)齊。
4.4 支持 【打開(kāi)flutter頁(yè)面不再打開(kāi)容器】 場(chǎng)景
在Flutter模塊內(nèi)部,F(xiàn)lutter 頁(yè)面跳轉(zhuǎn)Flutter 頁(yè)面是可以不需要再打開(kāi)Flutter容器的,不打開(kāi)容器,能節(jié)省內(nèi)存開(kāi)銷(xiāo)。在FlutterBoost3.0上,打開(kāi)容器和不打開(kāi)容器的區(qū)別表現(xiàn)在用戶(hù)接口上僅僅是withContainer參數(shù)是否為true就好。
InkWell( child: Container( color: Colors.yellow, child: Text( '打開(kāi)外部路由', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => BoostNavigator.of().push("flutterPage", arguments: <String, String>{'from': widget.uniqueId}), ), InkWell( child: Container( color: Colors.yellow, child: Text( '打開(kāi)內(nèi)部路由', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => BoostNavigator.of().push("flutterPage", withContainer: true, arguments: <String, String>{'from': widget.uniqueId}), )
4.5 生命周期的精準(zhǔn)通知
在FlutterBoost2.0上,每個(gè)頁(yè)面都會(huì)收到頁(yè)面生命周期通知,而FlutterBoost3.0只會(huì)通知頁(yè)面可見(jiàn)性實(shí)際發(fā)生了變化的頁(yè)面,接口也更符合flutter的設(shè)計(jì)。
4.6 其他Issue
除了上面的一些特性外,F(xiàn)lutter Boost 3.0版本還解決了如下一些問(wèn)題:
- 頁(yè)面關(guān)閉后參數(shù)的傳遞,之前只有iOS支持,android不支持,目前在dart側(cè)實(shí)現(xiàn),Ios 和Android 都支持。
- 解決了Android 狀態(tài)欄字體和顏色問(wèn)題。
- 解決了頁(yè)面回退willpopscope不起作用問(wèn)題。
- 解決了不在棧頂?shù)捻?yè)面也收到生命周期回調(diào)的問(wèn)題
- 解決了多次setState耗性能問(wèn)題。
- 提供了Framgent 多種接入方式的Demo,方便tab 場(chǎng)景的接入。
- 生命周期的回調(diào)代碼,可以用戶(hù)代碼里面with的方式接入,使用更簡(jiǎn)單。
- 全面簡(jiǎn)化了,接入成本,包括 dart側(cè),android側(cè)和ios
- 豐富了demo,包含了基本場(chǎng)景,方便用戶(hù)接入 和測(cè)試回歸
到此這篇關(guān)于Flutter Boost 混合開(kāi)發(fā)框架的文章就介紹到這了,更多相關(guān)Flutter Boost內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!,希望大家以后多多支持腳本之家!
相關(guān)文章
如何使用IOS自動(dòng)化測(cè)試工具UIAutomation
這篇文章主要介紹了UIAutomation使用實(shí)例、應(yīng)用技巧、基本知識(shí)點(diǎn)總結(jié)和需要注意事項(xiàng),具有一定的參考價(jià)值2021-04-04右滑返回手勢(shì)和UIScrollView中手勢(shì)沖突的解決方法
這篇文章主要為大家詳細(xì)介紹了右滑返回手勢(shì)和UIScrollView中手勢(shì)沖突的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02詳解iOS開(kāi)發(fā)中的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)和組動(dòng)畫(huà)以及UIView封裝動(dòng)畫(huà)
這篇文章主要介紹了iOS開(kāi)發(fā)中的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)和組動(dòng)畫(huà)以及UIView封裝動(dòng)畫(huà),主要用到了CAAnimation類(lèi)和UIView類(lèi),需要的朋友可以參考下2015-11-11iOS測(cè)試手機(jī)APP的方法匯總:真機(jī)運(yùn)行,打ipa包,testFlighe,蒲公英
這篇文章主要介紹了iOS通常測(cè)試手機(jī)APP的四種方法:真機(jī)運(yùn)行,打ipa包,(testFlighe)郵件,蒲公英測(cè)試。需要的朋友可以參考下2022-12-12IOS LaunchScreen設(shè)置啟動(dòng)圖片與啟動(dòng)頁(yè)停留時(shí)間詳解
這篇文章主要介紹了IOS LaunchScreen設(shè)置啟動(dòng)圖片與啟動(dòng)頁(yè)停留時(shí)間詳解的相關(guān)資料,需要的朋友可以參考下2017-02-02IOS 應(yīng)用程序管理的實(shí)現(xiàn)
這篇文章主要介紹了IOS 應(yīng)用程序管理的實(shí)現(xiàn)的相關(guān)資料,希望通過(guò)本文能幫助到大家,讓大家實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-10-10iOS 實(shí)現(xiàn)類(lèi)似QQ分組樣式的兩種方式
這篇文章主要介紹了iOS 實(shí)現(xiàn)類(lèi)似QQ分組樣式的兩種方式,思路很簡(jiǎn)單,對(duì)模型數(shù)據(jù)操作或則控制界面顯示,需要的朋友可以參考下2017-07-07