Flutter与已有iOS工程混合开发与脚本配置

做者:Realank Liu
连接:https://juejin.im/post/5b7a1bfbe51d4538a93d2339
来源:掘金
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

本文转自⬆️, 并结合本身的实践对其中一些地方作修改, 使用 Swift 语言.android

修改的地方用"注释*"标出.


运行一个原生的Flutter工程(也就是纯Flutter)很是简便,不过如今Flutter属于试水阶段,要是想在商业app中使用Flutter,目前基本上是将Flutter的页面嵌入到目前先有的iOS或者安卓工程,目前讲混合开发的文章有不少:ios

Flutter新锐专家之路:混合开发篇git

Flutter混合工程改造实践github

Flutter混合工程开发探究xcode

Now直播iOS Flutter混合工程实践bash

不过这些文章大多讲的是安卓和flutter混合开发的,没有iOS和Flutter混合开发的比较详细的步骤实操,上周试了一下iOS和Flutter混合,有一些坑,总结给你们app

1.目的

既然用Flutter混合开发,那确定是但愿写一套代码,安卓iOS都能无负担运行,因此在开发的时候,须要知足以下需求:iphone

  • Flutter、iOS、安卓工程的目录在同一级,互相以前平级、无嵌套
  • 开发iOS的时候,不用操心Flutter部分,只用xcode点击运行就能够(即修改编译iOS项目时,使用编译好的Flutter产物)
  • 开发Flutter的时候,不用操心iOS部分,只用android studio点击运行就能够
  • 支持模拟器和真机

混合开发最权威的指南固然是flutter本身的wiki,可是缺陷是iOS部分,自动运行脚本的内容不够详细,项目结构也不利于混合开发,本文以其为基础,又对目录结构和脚本作了一些修改,使其便于维护ide


2.项目搭建
函数

2.1 文件目录搭建

HybridFlutter
    |-iOS
    |-Android
    |-Flutter
    |-build
复制代码复制代码

2.2 iOS项目搭建

创建完了上图文件目录,添加iOS工程(安卓工程暂时忽略)

而且在第一页VC上增长一个Next按钮,集成好Flutter之后,点击Next能够进入Flutter页面

由于咱们要推入flutter页面,因此须要有navigation controller:

目前Flutter混合开发还不支持bit code,因此在iOS工程里关闭

2.3 Flutter Module搭建

这里有一个坑,按照flutter官方文档,下载的flutter工具对应其beta分支,是不支持生成Flutter module的,而混合开发的wiki里说,须要创建这么个module,经过咨询大牛,须要切换到master分支,而flutter有个channel命令,能够切换工具分支:

若是你不在master分支,请执行flutter channel master

以后在Flutter目录下执行flutter create -t module flutter_module

这样就建立好了flutter module

目前为止的目录结构

2.4 添加胶水文件

混合开发最关键的是将两个项目衔接起来,因此须要一些配置

2.4.1 xcconfig文件

首先是xcode工程配置的衔接,打开ios工程,在xcode中点击File->New->File添加Configuration Settings File文件,命名为FlutterConfig.xcconfig,

注意添加的路径是HybridFlutter/Flutter/flutter_module

此时可能xcode会在ios工程里添加了一个FlutterConfig.xcconfig文件的引用,为了项目干净,能够删除这个引用(可是不要删除文件)

在FlutterConfig.xcconfig里添加 #include "./.ios/Flutter/Generated.xcconfig" 引用flutter_module下的ios插件里的Generated.xcconfig文件

上面是给flutter添加xcconfig文件,下载添加ios工程里的xccofig文件Debug.xcconfig,并引用FlutterConfig.xcconfig(若是iOS工程里已经有了xcconfig文件,那么直接在已有的xcconfig里添加)

添加内容 #include "../../../Flutter/flutter_module/FlutterConfig.xcconfig"

而后,将Debug.xcconfig添加到iOS项目的Info-Configuration里:

2.4.2 AppFrameworkInfo.plist

这个文件在最新的flutter工具里已经自动建立好了 刚才咱们看的文件目录,不包含隐藏文件,其实flutter_module里还有对应的ios和android插件工程,都是隐藏文件,从隐藏文件里能够看到AppFrameworkInfo.plist

2.4.3 引入xcode-backend.sh

在ios工程里添加运行脚本"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build,而且确保Run Script这一行在 "Target dependencies" 或者 "Check Pods Manifest.lock"后面

此时点击xcode的运行,会执行到xcode-backend.sh脚本,因此不只会编译安装iOS app到模拟器(暂时运行对象是模拟器),并且在iOS工程目录,也会生成一个Flutter文件夹,里面是Flutter工程的产物

把这些产物放到iOS工程里,就能获取到flutter的资源了。

注释*: 本人在尝试以上下划线部分运行 xcode 后, 并无在本地生成 Flutter 文件夹.解决办法是, 到 Flutter 项目目录下, "shift + commond + >" 让隐藏的 .iOS 文件夹显示出来, 复制以下三个文件夹


而后到 iOS 项目下, 在一级 HybridiOS 目录下建立 Flutter 文件夹(和项目文件夹 Hybrid iOS 并列), 而后将刚才复制的三个文件粘贴到 Flutter 文件夹中.



进入 iOS 项目右键 Add files to......, 将刚才的 Flutter 文件夹添加到项目中




2.4.4 添加flutter编译产物

,将iOS工程目录下的Flutter文件夹添加到工程,而后确保文件夹下的两个framework添加到Embeded Binaries里

注释*: 因为 Flutter 文件夹并不在 HybridiOS 中, 此处选择Add other 添加, Copy Bundle Resources 的添加一样选择 Add other




确保flutter_aseets添加到Build Phases里的Copy Bundle Resources里

添加完,在工程目录里,会多出一个flutter _aseets引用(注意只是引用,若是是拷贝可能会有问题),实际上是引用的Flutter/flutter _aseets,试了半天没有去掉,就先这样吧

目前,全部的胶水文件都已经添加完了,下一步就是在iOS工程里,显示flutter页面

3. 引用Flutter页面

注释*: 3.1 部分 AppDelegate改造

 FlutterPluginAppLifeCycleDelegate 只有对于使用插件的才须要写, 没有使用插件不须要. 3.1 部分可直接忽略看下边注释* 

注释*: 此处本人使用的是 Swift 语言, 直接贴出 AppDelegate 的代码.

修改部分:

1. import Flutter 

2.AppDelegate: UIResponder, UIApplicationDelegate改成AppDelegate: FlutterAppDelegate 

3.didFinishLaunch 的返回值 return true 改成  return super.application(application, didFinishLaunchingWithOptions: launchOptions); 4.AppDelegate 全部代理函数前加 override

import UIKit
import Flutter


@UIApplicationMain
class AppDelegate: FlutterAppDelegate {


    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return super.application(application, didFinishLaunchingWithOptions: launchOptions);
    }


    override func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    }


    override func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }


    override func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    }


    override func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }


    override func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }
}


复制代码


3.1 AppDelegate改造

改变AppDelegate.h,使其父类指向FlutterAppDelegate:

#import <Flutter/Flutter.h>

@interface AppDelegate : FlutterAppDelegate <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@end
复制代码复制代码

改造AppDelegate.m

//
//  AppDelegate.m
//  HybridIOS
//
//  Created by Realank on 2018/8/20.
//  Copyright © 2018年 Realank. All rights reserved.
//

#import "AppDelegate.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

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

- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
    return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions: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];
}

- (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
    [_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}

- (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];
}

- (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 (^)(void))completionHandler {
    [_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
                  completionHandler:completionHandler];
}

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

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

@end


复制代码复制代码

这部分改造的原理尚未深究,并且有一些方法的实现iOS已经提示弃用了,你们在加入已有工程的时候,须要酌情考虑,我相信后续flutter官方也会更新相关的方法


3.2 推入flutter页面

在首页VC中添加以下代码

//
//  ViewController.m
//  HybridIOS
//
//  Created by Realank on 2018/8/20.
//  Copyright © 2018年 Realank. All rights reserved.
//

#import "ViewController.h"
#import <Flutter/Flutter.h>
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (IBAction)goNext:(id)sender {
    FlutterViewController* flutterViewController = [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
    FlutterBasicMessageChannel* messageChannel = [FlutterBasicMessageChannel messageChannelWithName:@"channel"
                                                        binaryMessenger:flutterViewController
                                                                  codec:[FlutterStandardMessageCodec sharedInstance]];//消息发送代码,本文不作解释
    __weak __typeof(self) weakSelf = self;
    [messageChannel setMessageHandler:^(id message, FlutterReply reply) {
        // Any message on this channel pops the Flutter view.
        [[weakSelf navigationController] popViewControllerAnimated:YES];
        reply(@"");
    }];
    NSAssert([self navigationController], @"Must have a NaviationController");
    [[self navigationController]  pushViewController:flutterViewController animated:YES];
}

@end

复制代码复制代码

若是你的首页不在navigation controller里,那么pushflutter页面确定会报错,这和flutter不要紧,若是确实没有navigation controller,能够present flutterViewController

运行代码,点击next,就能够看到flutter页面了:

由于咱们的导航栏使用了iOS原生的,因此flutter的导航栏有点多余了,咱们去掉flutter导航栏:

再次运行:

证实改动能够同步到app

3.3 flutter页面管理

你可能发现了,上面的代码运行的时候,在flutter页面点击右下角的加号能够增长中间的数字,可是当退出当前页面,再进入flutter页面之后,中间的数字又重置为0了,这是由于每次点击Next,都会从新分配和初始化全部flutter资源,这形成了flutter页面启动慢,状态没法保存(这个页面的数字状态不必保存,可是别的场景下必定有须要保存的内容)

因此Flutter新锐专家之路:混合开发篇对混合开发中flutter部分作了很好的管理,它将flutter部分作成单例,使其基础资源在app运行期间只运行一次,再将flutter根页面设置成一个空白container,须要flutter推入什么页面,就发消息给flutter,flutter在空白container基础上推入对应页面,这样当从flutter的某个页面回退到iOS原生页面的时候,flutter也会释放掉刚刚显示的页面,回退到空白页面。

4. 配置自动运行脚本

针对怎么写代码,不是这篇文章的范畴,下面说说混合开发最后的一个痛点

如今的工程,flutter部分有改动,能够直接经过绑定的xcode-backend.sh来编译,并生成framework和资源文件,因此不管是iOS端,仍是flutter端有改动,在xcode上点击run均可以运行到模拟器和真机,并且iOS和flutter项目代码彼此独立,只有flutter的编译产物留在了iOS文件夹里 可是如今还有一个问题,就是当开发flutter部分的时候,咱们并不想碰xcode,最好能关掉xcode,只打开android studio作开发,而后点击AS上的run按钮运行。

4.1 实现原理

  • xcode命令行工具,能够编译iOS项目(就像xcode里点击run同样),而且还能指定生成.app文件的目录
  • flutter运行的时候,能够指定--use-application-binary,flutter编译产物,以hot-load的方式注入到指定app中(这个原理是我本身猜的,实际状况待仔细确认)

经过上述两步,就能够在android studio里,直接往iOS系统里安装混合app了

4.2 模拟器实现

用android studio打开flutter_module文件夹

能够看到右上角已是能够run的状态了,可是点击的话,会有以下错误提示:

缘由很简单,这个flutter_module不是一个独立的工程,须要依赖一个app,因此咱们须要先编译出iOS app,并放到好找的位置:

点击下图的Edit Configurations

而后添加一个运行前编译app的命令,点击下图的Run External tool

添加下面的一条:

Program里填/usr/bin/env,Arguments里填xcrun xcodebuild build -configuration Debug VERBOSE_SCRIPT_LOGGING=YES -project ../../iOS/HybridIOS/HybridIOS.xcodeproj -scheme HybridIOS BUILD_DIR=../build/ios -sdk iphonesimulator -arch x86_64,这里面指定了编译的参数

添加后如图:

接着添加flutter编译的参数,指定刚刚编译出来的app做为hotload的宿主app: --use-application-binary /Users/realank/Documents/GitHub/HybridFlutter/iOS/build/ios/Debug-iphonesimulator/HybridIOS.app 这里须要注意,我一开始使用相对路径,怎么也运行不起来,说找不到对应的app,因此我使用了绝对路径,你要换成本身的HybridFlutter/iOS/build/ios/Debug-iphonesimulator/HybridIOS.app的绝对路径

大功告成,这时候点击run运行,就会先编译ipa,在运行flutter

4.3 真机

真机是同样的原理,就是命令参数不同:

运行flutter前编译app的命令:xcrun xcodebuild build -configuration Debug VERBOSE_SCRIPT_LOGGING=YES -project ../../iOS/HybridIOS/HybridIOS.xcodeproj -scheme HybridIOS BUILD_DIR=../build/ios -sdk iphoneos -arch arm64

真机的app和模拟机app的产物路径不同,因此flutter参数也得变: --use-application-binary /Users/realank/Documents/GitHub/HybridFlutter/iOS/build/ios/Debug-iphoneos/HybridIOS.app

这样,咱们就能够选择想要运行的是真机仍是模拟器,而后点击run运行

5 总结

flutter混合开发,须要手动设置的地方不少,可是一旦设置好,就不须要再改动,至于最后的flutter运行参数,须要指定绝对路径,不知道什么缘由,好在影响不大,有空再仔细研究。但愿本文会对你有帮助









相关文章
相关标签/搜索