`编程`分类下的文章

编程

iOS线性布局?

前几天通过比较了iOS和Android端BLE蓝牙开发时,订阅Characteristic的不同,得出了Android订阅特征相当复杂的结论。但是在结尾我也替Android开发洗了一次白,因为今天说的这个东西,在iOS上非常难搞,而在Android上相当容易!

Context

新的码表产品,是一款为广大骑行爱好者省钱的GPS码表!

这个硬件会记录骑行时候的速度,位置,海拔,心率,踏频,等等等等信息,并在表盘显示实时的状态。所以,手机的用处就是,将骑行记录导出,然后对数据进行备份,为骑行者分析骑行过程。

咱们不仅仅是价格很厉害,在同步方式上也很厉害。因为我们可以通过Wi-Fi同步,而不仅仅是蓝牙同步。Wi-Fi的速度可比蓝牙高得多。不过这些都不是今天的主题,今天是要说app中,展示某一次骑行详情时候的图表展示。

废话不多说,先上图

iOS
Android

这里的图表的表项,是不确定数量的。比如说,这个用户没有踏频器的话,那么数据分析中就会没有踏频的图表。没有心率计的话,就会没有心率的图表。

Android

Android实在是,太方便了吧!

用了LinearLayout之后,我们把所有有可能出线的图表,都写在LinearLayout中,外面再套一个ScrollView。

不需要的图表我们直接把它setVisibility(View.GONE)就完事儿了,后面的图表会紧紧的挨上来,不会留出一个大空白来。

而且ScrollView会随着LinearLayout的大小来确定是否可以滑动,最多可以滑动多少距离。

iOS

没有LinearLayout,只有frame或者auto layout,你说怎么办?

用框架!

是的,有MyLinearLayout,很方便,就是对标Android的LinearLayout做的,其原理也是逃不掉frame和auto layout的。

如果我用框架做,那还写这篇干什么呢~

因为如果不手动写,是完全不会遇到UIScrollView和AutoLayout的一个大坑的!

Step 1

想法很简单,我只要给一个变量叫做lastChart,其实就好了~

@interface ChartViewController () {

    UIView *lastChart; //用了表示上一个图表的指针

    UIScrollView *scrollV;
    UIView *contentView;
    __weak ActivityDetailPresenter *presenterReference;

    ChartBlockView *distanceView;
    ChartBlockView *timeView;
    ChartBlockView *cadenceView;
    ChartBlockView *heartView;

}
@end

这里ChartBlockView表示图表,他是UIView的子类,所以lastChart完完全全可以引用他们。

Step 2

每次从Presenter中reload图表数据的时候,我会一个图表一个图表的去渲染。

比如,我先去检查,有没有速度信息,有的话就搞一个速度的表,没有的话就跳过。

再获取踏频的数据,有数据搞一个踏频的表,没有的话就跳过。

每次在渲染一个表的时候,会去检查lastChart是否为nil,如果为nil,那top就以顶端为参考;如果不为nil,那top就以lastChart的bottom做为参考。

根据这个准则,便很容易实现。

Step 3

当我兴高采烈的run了之后,发现可以显示,但是scrollview完全不能滑动!

查看了view Hierarchy,发现UIScrollView的ContentSize竟然是(0,0)!

因为……autolayout并不能填充scrollview……

所以,后来我又花了些时间,解决了这个问题……

End

所以,iOS没有LinearLayout还是蛮讨厌的……

虽然iOS原生推出了一个新的控件,就是LinearLayout,但是需要iOS 11……

预知后事如何,请听下回分解……

阅读剩下更多

编程

扫码登录(3)

Context

Http是在tcp的基础上做了高级封装,变成了一个被动的,短链接请求.请求完了就把连接断开了,下一次要请求,就需要再一次进行tcp握手,然后通信……在前面第一篇中,我用了定时轮训的方式问服务器,当前这个二维码是否登录,从功能的角度来说,应该是没有问题的,而且写起来也是特别的方便.

不过仔细的想一想,其实还是比较浪费资源的.从信息论的角度来说,中间的N次质询,相当于没有任何的信息量(请不要深究这一句),所以,我们不用http来更新扫码状态!

我们用

Web Socket!

Web Socket

关于websocket的定义,解释什么的,百度一下,你全都知道了,所以我就不在这个地方多说.

只说明一点,websocket屌的飞起,和socket的操作一毛一样,关键是服务器终于可以主动的发消息给客户端啦!

他算是一个长连接,握手之后,就开始了肮脏的通信,通信完了,一方可以发起关闭操作,然后……,最后连接就拆掉了.

Django

websocket怎么说也就只能算是对web服务器的一个扩充,如何在已有的web服务器上快速集成一个websocket的服务,才是比较重要的~

由于之前用的是django做服务器,所以我们就找找给django加上websocket的方法~

在他的官方文档中提到了很多可用的开源app,其中有一个叫做channels的库,他用了asgi,直接可以快速集成在django中,牛的一批.

Channels

这个库的文档,真的是无力吐槽,我真是集成的很辛苦

我们来简化一下他的文档吧!

Router

和django一样,websocket的连接也要有个路由的,我们先建立一个py文件

from channels.routing import route
# 这个文件等会儿解释
from users.webClient import connection_handler, disconnection_handler,subscribe_token

channel_routing = [
    route("websocket.connect", connection_handler, path=r"/ws/$"),
    route("websocket.disconnect", disconnection_handler, path=r"/ws/$"),
    route("websocket.receive", subscribe_token, path=r"/ws/$"),
]

这里的websocket.connect,websocket.disconnect,websocket.receive相当于是websocket的三种事件,已连接、已断开、收到消息

中间那个参数和django里面的那个描述一样,就是该路由对应的处理方法.

Handler

现在来解释一下上一节说的 users.webClient这个文件

# -*- coding: utf-8 -*-

import json
from channels.sessions import channel_session
from users.qrcode_helper import *


@channel_session
def connection_handler(message):
    message.reply_channel.send({"text": json.dumps({"status":False,"msg":"connected"})})


@channel_session
def subscribe_token(message):
    try:
        obj = json.loads(message.content["text"])
        tokens = obj["token"]
        qrcode_record = fetch_qrcode_record_with_token(token=tokens)
        if qrcode_record is not None:
            if is_token_pass_the_authentic(tokens):
                message.reply_channel.send(
                    {"text": json.dumps({"status": True, "msg": "succeed"})})
            else:
                message.reply_channel.send(
                    {"text": json.dumps({"status": False, "msg": "succeed"})})
                qrcode_record.webSocket_session = str(message.content["reply_channel"])
                qrcode_record.save()
                # link token to this channel
        else:
            message.reply_channel.send({"text": json.dumps({"status": "-1", "msg": "token invalid"})})
            message.reply_channel.send({"close": True})
    except Exception as e:
        message.reply_channel.send({"text": e.message})
        message.reply_channel.send({"close": True})


@channel_session
def disconnection_handler(message):
    channel = str(message.content["reply_channel"])
    qrcode_records = fetch_qrcode_record_with_web_socket_channel(channel)
    if qrcode_records is not None:
        for qrcode_record in qrcode_records:
            qrcode_record.webSocket_session = ""
            qrcode_record.save()

很长,也不用仔细看,因为里面主要做了扫码登录里面的事情,如果只是要集成一下websocket的话,不用关心这些方法里面的操作,只要注意这里每个方法的传入参数 message

Message有那些用处呢?

1.通过message.channel或者message.reply_channel可以获取到连接的通道(请暂时不要使用高级操作)
2.message.content其实是一个字典,这个字典遵循了ASGI的消息模型,里面有text,reply_channel等等字段

在我的处理代码里面,我从message.content里面取出了reply_channel,他的value是一个长得很奇怪的字符串(我把它类比成文件描述符把),毕竟要有很多很多很多连接的话,它们的文件描述符不能重复对吧,所以就又长又难看,比如像daphne.response.FSITXWDDzG!IOtDNILqzh

有了这个reply_channel,在后面主动发送消息下去的时候,就可以指定连接发送了~

Attention

注意,这里的websocket.connect的handler里面一定一定,千万千万要给反馈消息回去,不然浏览器(只测试了Chrome的)会以为连接超时的!然后给你一个503 bad gateway,之后就主动断开连接了,留你一脸蒙逼的在那儿傻看着.

Send

现在我们可以看看之前第一章中的app调用的扫码接口了

@csrf_exempt
@require_POST
@pass_auth
@require_parameter(["code"])
def allow_the_qrcode_login(request):
    code = request.POST["code"]
    user = get_user_from_response_session(request)
    qr_record = fetch_qrcode_record_with_code(code)
    if user is not None and qr_record is not None:
        if qr_record.user_id is not None:
            return JsonResponse({"msg": "expired", "status": -1}, status=400)
        qr_record.user_id = user.user_uuid
        qr_record.status = True
        qr_record.save()
        if qr_record.webSocket_session is not None:
            # send new status to the web socket
            channel = Channel(name=qr_record.webSocket_session)
            content = {"text": json.dumps({"status": True, "msg": "succeed"})}
            channel.send(content)
            pass
        return JsonResponse({"msg": "succeed", "status": 0}, status=200)
    return JsonResponse({"msg": "code not existed", "status": -400}, status=400)

这里加了一些些小的改动,我再把他取出来

if qr_record.webSocket_session is not None:
    channel = Channel(name=qr_record.webSocket_session)
    content = {"text": json.dumps({"status": True, "msg": "succeed"})}
    channel.send(content)

websocket.receive的时候,我把websocket的文件描述符记录在了qrcode的记录中,这样扫描某一个qrcode的时候,就可以知道这个二维码是否有websocket正在订阅他,有订阅的话,就可以拿来对指定的通道进行发送了

End

网页端真的没什么好说的,直接贴(里面没有很严密的控制权限,稍微改一改就行,但是我懒)

function listen_for_status() {
    socket = new WebSocket("ws://" + window.location.host + '/ws/');
    socket.onmessage = function (e) {
        var obj = JSON.parse(e.data);
        if (obj.status == true) {
            socket.close();
            window.location="dashboard.html";
        }
    };
    socket.onopen = function () {
        var obj = {};
        obj.token = token;
        socket.send(JSON.stringify(obj));
    };
}

还有不要忘记更新nginx的配置

扫码登录的这个小玩意儿,虽然看起来很是简单,但是也耗了我很多的时间,这个过程中,也是学到了很多新的东西呢!

这次也是达成了一个新的成就:WebSocket(Server&Client)

阅读剩下更多

返回顶部