在原生ios項目中集成flutter

概述

本文不想寫一個全篇步驟式的文章來描寫怎麼集成flutter,而是指望用一種探索的方式來追尋答案。ios

原理分析

咱們首先看下flutter項目和通常原生項目的大概區別。git

爲了跳轉方便,原生項目的入口通常是UINavigationControllergithub

而咱們看下flutter默認給咱們建立的模板爲:shell

clipboard.png

clipboard.png

這裏咱們來看下flutter的引擎源碼,看下這段代碼作了什麼工做,源碼路徑爲:https://github.com/flutter/en...xcode

咱們首先看下`FlutterAppDelegateapp

https://github.com/flutter/en...iphone

- (instancetype)init {
  if (self = [super init]) {
    _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
  }
  return self;
}

....

- (BOOL)application:(UIApplication*)application
    willFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
  return [_lifeCycleDelegate application:application willFinishLaunchingWithOptions:launchOptions];
}
....

因此這裏能夠看到,FlutterAppDelegate徹底是調用了FlutterPluginAppLifeCycleDelegate的全部方法。假設你的項目原先就有一個AppDelegate的實現類,那麼能夠參考FlutterAppDelegate的源碼,建立一個FlutterPluginAppLifeCycleDelegate,並在全部方法中調用這個類實例的方法。ide

原生項目中建立根ViewControler的方式可使用StoryBoard,也可使用代碼建立。而flutter模板給咱們建立的項目爲StoryBoard的方式fetch

clipboard.png

clipboard.png

從這裏咱們能夠發現,flutter默認項目模板是將FlutterViewController做爲根ViewController。優化

項目實戰

建立項目

原理分析完畢,咱們能夠建立一個工程項目了.

咱們這裏選擇建立一個最多見的SingleViewApp

clipboard.png

clipboard.png

改爲不使用StoryBoard,而是代碼建立根ViewController

clipboard.png

爲了演示方便,咱們建立一個controller
clipboard.png

修改一下啓動代碼:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    UIViewController* main = [[MainViewController alloc]initWithNibName:@"MainViewController" bundle:nil];
    UINavigationController* root = [[UINavigationController alloc]initWithRootViewController:main];
    self.window.backgroundColor = [UIColor whiteColor];
    self.window.rootViewController = root;
    [self.window makeKeyAndVisible];
    return YES;
}

在MainViewController中,咱們擺上兩個按鈕:

clipboard.png

建立flutter模塊

咱們使用flutter自帶命令建立一個flutter模塊項目

flutter create -t module my_flutter

把建立出來的全部文件一塊兒拷貝到上面ios原生項目的同一級目錄中:

clipboard.png

使用pod初始化一下項目:

cd myproject
pod init

這樣就生成了Podfile

clipboard.png

咱們打開修改一下,以便將flutter包括在裏面

platform :ios, '9.0'
target 'myproject' do

end

#新添加的代碼
flutter_application_path = '../'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

運行下pod安裝

pod install

clipboard.png

咱們能夠看到,與剛纔相比,新增長了workspace文件,咱們關掉原來的項目,並打開workspace

clipboard.png

而後咱們能夠看到項目結構以下:

clipboard.png

編譯一下:

ld: '/Users/jzoom/SourceCode/myproject/myproject/DerivedData/myproject/Build/Products/Debug-iphoneos/FlutterPluginRegistrant/libFlutterPluginRegistrant.a(GeneratedPluginRegistrant.o)' does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. file '/Users/jzoom/SourceCode/myproject/myproject/DerivedData/myproject/Build/Products/Debug-iphoneos/FlutterPluginRegistrant/libFlutterPluginRegistrant.a' for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

出現了這個錯誤

打開項目編譯配置,並搜索bit,出現下面結果:

clipboard.png

修改下Enable Bitcode爲No

clipboard.png

此時編譯ok。

至此,在原生項目中配置flutter完畢,咱們開始開發功能。

修改AppDelegate

因爲咱們的AppDelegate不是FlutterAppDelegate,因此咱們按照前面分析的路子,改爲以下:

//
//  AppDelegate.m
//  myproject
//
//  Created by JZoom on 2019/4/9.
//  Copyright © 2019 JZoom. All rights reserved.
//

#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
#import <Flutter/Flutter.h>
#import "MainViewController.h"


@interface AppDelegate()<FlutterPluginRegistry>

@end

@implementation AppDelegate{
    FlutterPluginAppLifeCycleDelegate* _lifeCycleDelegate;
}


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    UIViewController* main = [[MainViewController alloc]initWithNibName:@"MainViewController" bundle:nil];
    UINavigationController* root = [[UINavigationController alloc]initWithRootViewController:main];
    self.window.backgroundColor = [UIColor whiteColor];
    self.window.rootViewController = root;
    [self.window makeKeyAndVisible];
    
    [GeneratedPluginRegistrant registerWithRegistry:self];
   return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}




- (instancetype)init {
    if (self = [super init]) {
        _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
    }
    return self;
}

- (void)dealloc {
    _lifeCycleDelegate = nil;
}

- (BOOL)application:(UIApplication*)application
willFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
    return [_lifeCycleDelegate application:application willFinishLaunchingWithOptions:launchOptions];
}



// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {
    UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
    if ([viewController isKindOfClass:[FlutterViewController class]]) {
        return (FlutterViewController*)viewController;
    }
    return nil;
}

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    [super touchesBegan:touches withEvent:event];
    
    // Pass status bar taps to key window Flutter rootViewController.
    if (self.rootFlutterViewController != nil) {
        [self.rootFlutterViewController handleStatusBarTouches:event];
    }
}

- (void)applicationDidEnterBackground:(UIApplication*)application {
    [_lifeCycleDelegate applicationDidEnterBackground:application];
}

- (void)applicationWillEnterForeground:(UIApplication*)application {
    [_lifeCycleDelegate applicationWillEnterForeground:application];
}

- (void)applicationWillResignActive:(UIApplication*)application {
    [_lifeCycleDelegate applicationWillResignActive:application];
}

- (void)applicationDidBecomeActive:(UIApplication*)application {
    [_lifeCycleDelegate applicationDidBecomeActive:application];
}

- (void)applicationWillTerminate:(UIApplication*)application {
    [_lifeCycleDelegate applicationWillTerminate:application];
}

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
- (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
    [_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}
#pragma GCC diagnostic pop

- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
    [_lifeCycleDelegate application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application
       didReceiveRemoteNotification:userInfo
             fetchCompletionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
didReceiveLocalNotification:(UILocalNotification*)notification {
    [_lifeCycleDelegate application:application didReceiveLocalNotification:notification];
}

- (void)userNotificationCenter:(UNUserNotificationCenter*)center
       willPresentNotification:(UNNotification*)notification
         withCompletionHandler:
(void (^)(UNNotificationPresentationOptions options))completionHandler
API_AVAILABLE(ios(10)) {
    if (@available(iOS 10.0, *)) {
        [_lifeCycleDelegate userNotificationCenter:center
                           willPresentNotification:notification
                             withCompletionHandler:completionHandler];
    }
}

- (BOOL)application:(UIApplication*)application
            openURL:(NSURL*)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
    return [_lifeCycleDelegate application:application openURL:url options:options];
}

- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
    return [_lifeCycleDelegate application:application handleOpenURL:url];
}

- (BOOL)application:(UIApplication*)application
            openURL:(NSURL*)url
  sourceApplication:(NSString*)sourceApplication
         annotation:(id)annotation {
    return [_lifeCycleDelegate application:application
                                   openURL:url
                         sourceApplication:sourceApplication
                                annotation:annotation];
}

- (void)application:(UIApplication*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
  completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {
    [_lifeCycleDelegate application:application
       performActionForShortcutItem:shortcutItem
                  completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
  completionHandler:(nonnull void (^)())completionHandler {
    [_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
                  completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 120000
- (BOOL)application:(UIApplication*)application
continueUserActivity:(NSUserActivity*)userActivity
 restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>>* __nullable
                              restorableObjects))restorationHandler {
#else
    - (BOOL)application:(UIApplication*)application
continueUserActivity:(NSUserActivity*)userActivity
restorationHandler:(void (^)(NSArray* __nullable restorableObjects))restorationHandler {
#endif
    return [_lifeCycleDelegate application:application
                      continueUserActivity:userActivity
                        restorationHandler:restorationHandler];
}
    
#pragma mark - FlutterPluginRegistry methods. All delegating to the rootViewController
    
- (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey {
    UIViewController* rootViewController = _window.rootViewController;
    if ([rootViewController isKindOfClass:[FlutterViewController class]]) {
        return
        [[(FlutterViewController*)rootViewController pluginRegistry] registrarForPlugin:pluginKey];
    }
    return nil;
}

- (BOOL)hasPlugin:(NSString*)pluginKey {
    UIViewController* rootViewController = _window.rootViewController;
    if ([rootViewController isKindOfClass:[FlutterViewController class]]) {
        return [[(FlutterViewController*)rootViewController pluginRegistry] hasPlugin:pluginKey];
    }
    return false;
}

- (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
    UIViewController* rootViewController = _window.rootViewController;
    if ([rootViewController isKindOfClass:[FlutterViewController class]]) {
        return [[(FlutterViewController*)rootViewController pluginRegistry]
                valuePublishedByPlugin:pluginKey];
    }
    return nil;
}

#pragma mark - FlutterAppLifeCycleProvider methods

- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
    [_lifeCycleDelegate addDelegate:delegate];
}
@end

建立FlutterViewController

編輯下MainViewController

- (IBAction)launchFlutter1:(id)sender {
    
    FlutterViewController* c = [[FlutterViewController alloc]init];
    [self.navigationController pushViewController:c animated:YES];
    
}

編譯下,運行點擊按鈕調取flutter視圖,發現一片空白,並出現以下錯誤:

2019-04-09 13:18:18.500285+0800 myproject[57815:1968395] [VERBOSE-1:callback_cache.cc(132)] Could not parse callback cache, aborting restore
2019-04-09 13:18:36.554643+0800 myproject[57815:1968395] Failed to find assets path for "Frameworks/App.framework/flutter_assets"
2019-04-09 13:18:36.658247+0800 myproject[57815:1969776] [VERBOSE-2:engine.cc(116)] Engine run configuration was invalid.
2019-04-09 13:18:36.659545+0800 myproject[57815:1969776] [VERBOSE-2:FlutterEngine.mm(294)] Could not launch engine with configuration.
2019-04-09 13:18:36.816199+0800 myproject[57815:1969793] flutter: Observatory listening on http://127.0.0.1:50167/

咱們看看和flutter本身建立的項目比,還差了什麼

clipboard.png

如圖:有三個地方,咱們把這些文件copy一份放到咱們的項目中,而且設置一下編譯選項:

clipboard.png

修改下項目的配置,增長一個腳本

clipboard.png

/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" thin

clipboard.png

放到Copy Bundle Resources下面

結果:

clipboard.png

進行優化

在上面的步驟裏面,咱們經過直接文件拷貝將.ios目錄下的flutter生成文件拷貝到了原生項目裏面,顯然咱們不能每一次都手動這麼作,咱們能夠添加一個命令來作這件事。

rm -rf ${SOURCE_ROOT}/Flutter/Generated.xcconfig
cp -ri ../.ios/Flutter/Generated.xcconfig ${SOURCE_ROOT}/Flutter/Generated.xcconfig
rm -rf ${SOURCE_ROOT}/Flutter/App.framework
cp -ri ../.ios/Flutter/App.framework ${SOURCE_ROOT}/Flutter/App.framework

咱們把這個命令放到前面去
clipboard.png

問題

Q : 如何調用flutter的不一樣頁面?

A : 咱們首先定義一下路由

clipboard.png

而後咱們能夠這麼調用

/// flutter的路由視圖
    FlutterViewController* c = [[FlutterViewController alloc]init];
    [c setInitialRoute:@"page2"];
    [self.navigationController pushViewController:c animated:YES];

Q : 如何在原生項目中調試flutter?
A : 首先在命令行啓動flutter的監聽

flutter attach

若是有多臺設備,須要選擇一下設備

flutter attach -d 設備標誌

而後就能夠在xcode中啓動調試運行項目

clipboard.png

改動代碼以後按下鍵盤上面的r鍵就能夠了。

相關文章
相關標籤/搜索