• Using Qt to build an Omi App for iOS (and Android)


    Working on projects where the technology is pre-determined, it’s often difficult to be able to deep-dive into familiar waters. Recently, we have been working on a new service called Omi - and in that context I was able to use two weeks to knock together a prototype of a mobile app for it using Qt and QML. With the help of QML, we were able to build a responsive and functional UX in less than two weeks

    What follows is a bit of a braindump of trips and ticks I found or discovered. For those expecting a more complete end-result, I apologise in advance; This is all I managed to cobble together before moving on to new projects ;)

    Initial project setup

    Omi App running on iOSInitially we created a project using the default “Qt Quick Application” template shipped with Qt Creator. This works pretty well and in my opinion Qt Creator is the best IDE to use for day-to-day development of Qt-based iOS apps – until you want to start packaging your App for testing or release. At this point you need to switch to Xcode to define launch image, icons etc – as well as various other .plist entries. I copied the pre-generated .plist and used it with QMAKE_INFO_PLIST = myapp.plist instead of having qmake overwrite things like the Facebook App ID.

    Networking

    Initially I started building a backend singleton where a QNetworkAccessManager subclass would marshal all calls to our backend API. However, after some helpful advice from our newest team member, Jens, I built up all the basic serverside interaction using only JavaScript and XMLHttpRequest.

    One key issue with Qt networking on iOS was caught in the response handlers for XMLHttpRequest. The status property would always return 0;

    var client = new XMLHttpRequest()
    client.open("GET", "https://api.domain.com/entity")
    client.onreadystatechanged = function() {
        console.log(client.status) // would always return '0'
    }
    client.send()


    As it turns out, this is because SSL support is not built-in due to export restrictions. That means you most likely need to compile in support for OpenSSL. I’ll leave that for a later post. If you have already done and have any helpful tips, please leave a comment in QTBUG-38864.

    Facebook login

    In mobile apps you typically authenticate using OAuth or similar technologies, where you end up with a token or cookie that you can re-use. To build the authorization token to pass to the server, Qt.btoa() is used to create a base64 string combining app id and secret;

    var client = new XMLHttpRequest();
    var authorizationHeader = "Basic "+Qt.btoa(AppVars.appKey+":"+AppVars.appSecret);
    client.setRequestHeader("Authorization", authorizationHeader);

    Then, the returned session token can be saved in persistent settings;

    import Qt.labs.settings 1.0
    
    Settings {
        id: settings
        property string token
    }
     
    onAuthenticated: {
        settings.token = authenticatedToken
    }


    This is naturally not very safe (and given it’s not passed over SSL it’s actually flat out dangerous in the current state)- it should rather be stored in the Keychain to prevent from outside tampering and inspection. But, it’s easy enough to hook that in at a later stage.

    Facebook login is provided more or less for you in the form of the FacebookSDK. Typically I would use CocoaPods to add this dependency in iOS Apps – but qmake breaks down with the .xcworkspace files required by CocoaPods. The alternative becomes manually adding the framework to your project, and linking against it;

    iphoneos {
        QMAKE_LFLAGS += -F /Users/hhartz/Documents/FacebookSDK/
        LIBS += -framework FacebookSDK
        HEADERS += qtfacebook.h
        OBJECTIVE_SOURCES += qtfacebook.mm
    }


    The qtfacebook.* files provide the API for login and response handling. Basically, it’s a simple stack object exposed to QML;

    QtFacebook facebook;
    engine.rootContext()->setContextProperty("facebook", &facebook);


    This can be called when the user clicks a facebook login button, where a slot is invoked to request a token for the App’s facebook ID;

    void QtFacebook::login()
    {
    if (FBSession.activeSession.state == FBSessionStateOpen || FBSession.activeSession.state == FBSessionStateOpenTokenExtended) {
        [FBSession.activeSession closeAndClearTokenInformation];
    } else {
        [FBSession openActiveSessionWithReadPermissions:@[@"public_profile"]
                                           allowLoginUI:YES
                                      completionHandler:
         ^(FBSession *session, FBSessionState state, NSError *error) {
           Q_UNUSED(state)
           Q_UNUSED(error)
           NSString *accessToken = session.accessTokenData.accessToken;
           if (accessToken.length>0) {
             emit(loggedInWithToken(QString(accessToken.UTF8String)));
           }
         }];
    }


    over in QML land, the loggedInWithToken signal is connected to a method that logs us on to our service with the supplied access token.

    Data model

    To represent the data model a Schema.js class is used which defines JavaScript objects using prototypes.

    function User(userJsonData) {
        this._id = userJsonData._id
        this.first_name = userJsonData.first_name
        this.middle_name = userJsonData.middle_name
        this.last_name = userJsonData.last_name
        this.email = userJsonData.email
    }
    
    User.prototype.avatarUrl = function() {
        var mailHash = Qt.md5(this.email.trim())
        return qsTr("http://www.gravatar.com/avatar/%1").arg(mailHash)
    }


    These are used when parsing the response JSON entities returned from the API, and stored in-memory.

    // assuming 'this' has a property friendsModel to the datastructure
    var friendsJSONArray = JSON.parse(client.responseText)
    friendsJSONArray.forEach(function(friendJSONData) {
       this.friendsModel.push(new Schema.User(object))
    })


    The challenging aspect of this technique is that QML mangles the objects, so if you have a JSON entity (e.g. an array) as a member of your object – it’s converted to a ListModel data entry. That naturally means using builtin JavaScript Array prototypes are out of the question and it kind of breaks the coding style.

    In the long run, it’s more flexible to build a native code model – or perhaps use a thirdparty solution that lets you represent JSON objects returned from a URI more elegantly. One very interesting approach is discussed in How Dropbox Uses C++ for Cross-Platform iOS and Android Development, which describes an architecture similar to Core Data (thanks to Jason)

    Native integration

    A key part of your iOS App are the UIApplicationDelegate protocal callbacks. This is used to implement support for push notifications, facebook login and many other things. However, Qt for iOS has no obvious hook to be able to respond to these callbacks. Categories to the rescue!

    You can use categories to override the builtin iOS app delegate, to allow e.g. integration with the iOS FacebookSDK;

    #import "QIOSApplicationDelegate+OmiAppDelegate.h"
    #import <FacebookSDK/FacebookSDK.h>
    
    @implementation QIOSApplicationDelegate (OmiAppDelegate)
    
    - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
    {
      return [FBAppCall handleOpenURL:url sourceApplication:sourceApplication];
    }
    
    @end


    Similar approaches would be used to implement support for push notificaitons.

    This can subsequently be included in your project by adding the relevant files to your project;
    OBJECTIVE_SOURCES += QIOSApplicationDelegate+MyAppDelegate.m

    Image Resources

    To add splash screen and icons, I highly recommend using Image catalogues. To add these to your iOS app, create a new Image catalogue, and add them to the xcodeproj before building for device. Unfortunately I’m not aware of a better way to include an image catalogue in your .pro file.

    UI Icons

    Since we use FontAwesome for our icons, it was rather trivial to get the same icons in the QML UI; Simply add FontAwesome as a resource, and load it in your QML.

    FontLoader {
        source: "qrc:/font/fontawesome-webfont.ttf"
    }


    Then, with the help of the (slightly outdated) fontawesome.js file from Using Fonts Awesome in QML it becomes trivial to create a nice flat icon;

    Text {
        font.family: "FontAwesome"
        text: FontAwesome.Icon.Reorder
        font.pixelSize: 20
    }


    Embedded in a circle, it looks like this;
    Menu button

    Scalability

    Jens came up with a nice trick to solve scalability (at least across his Android device and my iPhone 5s); define a

    property real scaleFactor: Math.min(window.width,window.height) / 320

    property which can be used all over the QML sources in place of fixed constants. This means we can define a font size to e.g.

    font.pixelSize: scaleFactor * fontSize

    Next steps?

    With the little time we invested in it, I think the App turned out pretty decent. There are still issues like Uncanny Valley effect to things like list bouncing etc which doesn’t feel 100% native. However, these are small details in the larger scheme of things which can be worked around or will be fixed upstream in the near future. It’s clear to me that Qt is a viable technology for Mobile Apps on iOS and Android and will help us in being able to build mobile Apps faster.

  • 相关阅读:
    spring使用中ModelAttribute的内容被覆盖
    html中,纯数字或纯英文的一串字符超出父容器不会折行显示,如何解决?
    js实现刷新页面出现随机背景图
    为tomcat配置项目必须的引擎文件
    mysql如何出查出最近7天,最近30天,最近n天的记录?
    为了显示此页面,Firefox 必须发送将重复此前动作的数据(例如搜索或者下订单)
    git 恢复到旧版本命令
    七大经典排序算法总结(C语言描述)
    C语言描述栈的实现及操作(链表实现)
    C语言的文件读写操作函数小结
  • 原文地址:https://www.cnblogs.com/bhlsheji/p/5169279.html
Copyright © 2020-2023  润新知