`iOS`标签下的文章

编程

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……

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

阅读剩下更多

编程

iOS图片取色

上周兴冲冲的在炫轮的iOS版中加了梦寐以求的GIF制作功能,这个功能两年前我就像加,只是当时胆子不够大,觉得iOS不能从相册读取GIF,所以这个功能就一直处于被砍掉的状态.

Not Important

用UIImagePickerViewController从图库中选择一个图片,这个百度一下,教程是有很多的,就不在这个本来就不重要的地方说了.

关键就在于如何处理选择的图片

在UIImagePickerViewController的delegate的回调方法中,有个info

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *, id> *)info

这个info可厉害了,里面有很多很多的东西

UIKIT_EXTERN NSString *const UIImagePickerControllerMediaType
UIKIT_EXTERN NSString *const UIImagePickerControllerOriginalImage //原图 UIImage,不过GIF只有第一帧
UIKIT_EXTERN NSString *const UIImagePickerControllerEditedImage
UIKIT_EXTERN NSString *const UIImagePickerControllerCropRect
UIKIT_EXTERN NSString *const UIImagePickerControllerMediaURL     UIKIT_EXTERN NSString *const UIImagePickerControllerReferenceURL //原图的文件地址
UIKIT_EXTERN NSString *const UIImagePickerControllerMediaMetadata
UIKIT_EXTERN NSString *const UIImagePickerControllerLivePhoto

直接使用UIImagePickerControllerOriginalImage获取UIImage肯定是不行的,这就是一个静图,所以我们使用UIImagePickerControllerReferenceURL获取到这个图片的位置,然后再获取到数据,然后再解析成GIF~完美

于是我就用NSData获取了这个url,然后发现,这个操作得到的NSData是nil!

我仔细一看这个url,是AssetLibrary的链接,不是一个绝对路径!

看来需要操作AssetLibrary了……

AssetLibrary && Photos

于是我就去Apple Developer官网找AssetLibrary的资料,吃鲸的发现,AssetLibrary已经灭绝(废弃)了!

这是天大的好消息啊,因为查看了其他的资料,据说AssetLibrary是非ARC的,还有一堆同步异步等乱七八糟的东西,总之就是坑很多.现在用Photos来代替,我紧张的去查询Photos,担心他要求的系统SDK比我当前部署的SDK高.

iOS 8 !

哈哈哈哈,和我当前的部署版本是一样的,这就意味着,这个版本的炫轮app的GIF功能是可以对所有用户开放的!

Photos依然有很多异步操作(毕竟有的图片可能是在iCloud上面的),所以我就干脆定义了一个block来

typedef void(^LoadingAssetBlock)(NSArray * images);

我是为了获取GIF的,那么这个返回的参数给个数组,就刚刚好了~

下面我们来根据url获取GIF吧!

- (void)imagesForURL:(NSURL *)url andBlock:(LoadingAssetBlock)block {
    PHFetchResult *assets= [PHAsset fetchAssetsWithALAssetURLs:@[url] options:nil]; // 1
    PHAsset *asset=[assets objectAtIndex:0]; // 2
    [[PHImageManager defaultManager] requestImageDataForAsset:asset options:nil resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { //3
        NSData *data=imageData;
        NSString *type=[NSData sd_contentTypeForImageData:data];  //这是SDWebImage的大佬们写的
        if ([type isEqualToString:@"image/gif"]) {
            UIImage *img = [UIImage sd_animatedGIFWithData:data];  这也是SDWebImage的大佬们写的
            NSMutableArray *renderedImage=[NSMutableArray array];
            for (UIImage *item in img.images) {
                VKMutableImage *mutable=[VKMutableImage imageWithImage:item];
                /**
                *这里真的不重要
                */
                [renderedImage addObject:mutable];
            }
            block([NSArray arrayWithArray:renderedImage]);
        } else{
            UIImage *image=[UIImage imageWithData:imageData];
            VKMutableImage *mutable=[VKMutableImage imageWithImage:image];
                /**
                *这里真的不重要
                */
            block(@[mutable]);
        }
    }];
}

其实只要看注释 1,2,3就够了……

They are not important!

我发现我好像跑题了,这一片的主要内容是,获取UIImage上的各个点的颜色的,然后我们使用RGB数组制作一个UIImage

再再介绍一下背景吧~

按理说上面说的这个图片操作的功能,早在炫轮刚面世的时候就存在的,为啥现在才来说这个东西呢?

因为从一开始就写错了!直到现在才发现

就当我上周兴冲冲的写完iOS的GIF制作功能之后,我就开始写android版的GIF制作功能,花了3天才终于把功能做完了!

正当我准备发一波朋友圈得瑟得瑟的时候,我发现了一个很严重的问题…

Android GIF ScreenShot
iOS GIF ScreenShot

使用了一样的调色,为什么结果不一样!按照以前的习惯,我一定是会怀疑Android版本出现了问题的,可是根据原图,我总觉得,Android上面的结果好像是正确的!

那就意味着,发布了快3年的炫轮iOS版App存在一个致命的图像处理的问题!

Here we go

正片开始了~

经过一连串debug手法,我找到了出问题的代码位置:以下是有问题的代码

CGImageRef imageRef=originImage.CGImage;
CGContextRef context = newBitmapRGBA8ContextFromImage(imageRef);
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
CGRect rect = CGRectMake(0, 0, width, height);
CGContextDrawImage(context, rect, imageRef);
unsigned char *bitmapData = (unsigned char *)CGBitmapContextGetData(context);
size_t bytesPerRow = CGBitmapContextGetBytesPerRow(context);
size_t bufferLength = bytesPerRow * height;
unsigned char *newBitmap = NULL;
if(bitmapData) {
    newBitmap = (unsigned char *)malloc(sizeof(unsigned char) * bytesPerRow * height);
    if(newBitmap) {
        for(int i = 0; i +3 < bufferLength;i=i+3) {
            int R=bitmapData[i];
            int G=bitmapData[i+1];
            int B=bitmapData[i+2];
            /**
            * 这里一点儿也不重要
            */
            newBitmap[i] = R;
            newBitmap[i+1]=G;
            newBitmap[i+2]=B;
            newBitmap[i+3]=255;
        }
    }
    free(bitmapData);
} else {
    NSLog(@"Error getting bitmap pixel data\n");
}    
CGContextRelease(context);
tempImage=[PublicHelper convertBitmapRGBA8ToUIImage:newBitmap withWidth:(int)width withHeight:(int)height];
free(newBitmap);
return tempImage;

newBitmap就是经过处理的图片的RGBA数组

关键错误应该是在这里:

int R=bitmapData[i];
int G=bitmapData[i+1];
int B=bitmapData[i+2];

虽然说我们知道图片是由一堆RGBA构成的,但是我们并不知道RGBA的顺序呀~这并不像Android里面那么爽,Color是一个32bit(4字节)的数字(int),每个字节分别代表着ARGB

那么iOS呢?

查了很多的资料,看到各位大佬都说iOS上面是RGBA,那么我原来的写法,好像很RGBA啊,好像没什么问题啊~

little-endian,big-endian

难道说,和大小端有关??

虽然说是RGBA,但是存在数组里面的是ABGR?

抱着试一试和重构的态度,借鉴了各种大佬的代码,之后改成了这样

const int imageWidth = originImage.size.width;
const int imageHeight = originImage.size.height;
size_t bytesPerRow = imageWidth * 4;
uint32_t* rgbImageBuf = (uint32_t*)malloc(bytesPerRow * imageHeight);    
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(rgbImageBuf, imageWidth, imageHeight, 8, bytesPerRow, colorSpace,kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast);
CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), originImage.CGImage);
int pixelNum = imageWidth * imageHeight;
uint32_t* pCurPtr = rgbImageBuf;
for (int i = 0; i < pixelNum; i++, pCurPtr++)
{
    uint8_t *ptr = (uint8_t*)pCurPtr;
    ptr[0] // Alpha
    ptr[1] // Blue
    ptr[2] // Greem
    ptr[3] // Red
    /**
     * 这里一点儿也不重要
     */
}
CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, rgbImageBuf, bytesPerRow * imageHeight,ProviderReleaseData);
CGImageRef imageRef = CGImageCreate(imageWidth, imageHeight, 8, 32, bytesPerRow, colorSpace,kCGImageAlphaLast | kCGBitmapByteOrder32Little, dataProvider,NULL, true, kCGRenderingIntentDefault);    
CGDataProviderRelease(dataProvider);
tempImage = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
return tempImage;

void ProviderReleaseData (void *info, const void *data, size_t size)
{
    free((void*)data);
}

现在用uint32_t来类比android中的Color

还用了DataProvider来把字节流转换成UIImage~这样修改后的代码,变得更加简洁了!

关键是,现在工作的正常了!

iOS GIF Right

End

不打不相识,Android和iOS不一起开发互相比较的话,就不能互相进步~

好了,这次不仅添加了GIF制作这个功能,还把以前的图片改色功能给修好了~真是一举两得啊!

演示视频

iOS<=====>Android

iOS这个视频里面的颜色修改,还没有改~.~

阅读剩下更多

编程

UIWindow 之坑

Context

做一个Pop View有两种方法.

1.用一个UIView盖在当前UIView或者UIWindow的最上面

2.做一个UIWindow,让他变成keyWindow

第一种的好处是…没想到什么特别的好处,大概写起来简单吧.

第二种的好处是,可以扔个ViewController在里面,哈哈哈.

而且第二种,我觉得做起来结构比较清晰.这个Pop View里面做的事情,可以全部封装好,包括显示和消失.
而使用UIView的话,做显示就一定需要把superview作为参数传入,才可以显示,这个还是蛮麻烦的,特别是当我
想做一个全局接收通知,并在UI上显示的东西的时候.由于很难确定当前最顶上的UIView是什么,所以还是UIWindow比较好.

Bug

然而,在水壶架iOS端中,遇到了一个很奇怪的问题.这个问题貌似只在iOS9.3.5之前的系统上出现.对于之后的系统,没有出现这个问题.

从Leancloud的错误报告中看到,这是一个消息传到了空指针的fetal错误.

那就好好找找,为啥变成了空指针吧.

Come on, Where are you

由于一开始这个问题只在9.3.5的手机上出现了,而我和一大票iOS10都没有这个问题.所以我甚至怀疑,这个是手机的问题.

直到我发现所有的老系统,都爆了!我才慌了.

当弹出这个搜索框,再让他消失的时候,做任何的触摸操作,整个app就挂掉了.

所以,问题肯定就在dismiss方法里面!

-(void)disappear
{
    [UIView animateWithDuration:0.3f animations:^{
        self.view.alpha=0;
    } completion:^(BOOL finished) {
        _displayWindows.rootViewController=nil;
            _displayWindows.hidden=YES;
            _displayWindows=nil;
        [[BluetoothManager sharedManager]stopScan];
        [BluetoothManager sharedManager].delegate=nil;
        if (self.delegate) {
            self.delegate=nil;
        }
    }];
}

每次disappear之后,我就中断了整个app,查看他的UI结构,发现有一个不听话的UIWindow还停留在哪儿~

而我明明都把他设置为hidden了,按理说它已经不是keyWindow了~可是他貌似还在.

而且为什么这个东西在iOS 10就没问题了呢~

于是我就去查了查iOS 10的开发这手册,看看他们都做了些什么改变,导致iOS 10不会闪退.

扯远了.

所以说,当这个alert级的UIWindow设置为hidden的时候,没有key window了……

于是乎Touch Event进来的时候,不知道应该给谁了,就爆掉了……

所以所以,得把之前的UIWindow重新恢复key window的地位.

之后改成这样了:

-(void)disappear
{
    [UIView animateWithDuration:0.3f animations:^{
        self.view.alpha=0;
    } completion:^(BOOL finished) {
        _displayWindows.windowLevel=UIWindowLevelNormal;
        _displayWindows.rootViewController=nil;
        [self.view removeFromSuperview];
            [_displayWindows setHidden:YES];
            _displayWindows=nil;
        [[BluetoothManager sharedManager]stopScan];
        [BluetoothManager sharedManager].delegate=nil;
        if (self.delegate) {
            self.delegate=nil;
        }
    }];
    if (_preWindows) {
        [_preWindows makeKeyAndVisible];
        _preWindows=nil;
    }
}

当然这样一改,在show的时候也要做一些修改了~得把之前的UIWindow给记录下来,不然就懵逼咯.

阅读剩下更多

编程

Block--方便而危险

Context

Block真的很好用对不对?记得很早以前用ASIHttp来做网络请求的时候,得用delegate,然后每次请求的
delegate回调中都要做一下判断,才能知道这个回调是回应了具体哪一次请求,这还是蛮麻烦的.

不过Block出现了,对网络请求的response操作,就可以直接写在block中,不用去判断这个response谁否对应与当前这个request.

除了网络请求,Alert的回调啊,asyncsocket啊,之类的异步操作,用了block都特别的方便.

Situation

然而,有一天我们在炫轮iOS端的DIY界面中发现了一个bug,而这个bug预示着,
我有实例没有成功释放.

Reason

于是我就一行一行的找,感觉没有什么地方出现.但是我发现了一个现象:

我在使用SCLAlertView了之后,就出现了这个bug,所以我就重点查看SCLAlertView的相关代码,
经过排查,我觉得问题只能出现在dismissBlock上.查看了dismissBlock的声明代码,他是个strong~

我想,这里应该是出现了一个循环引用…block中的内容,在声明的时候,会copy一份当时的数据.当AlertView 消失的时候,
调用了dismissBlock,然而,dismissBlock中的内容有引用当前的ViewController,所以.

他们互相引用了!

所以只能在dismissBlock的最后加一行,把这个dismissBlock设置为nil.解决了~

End

这次要不是遇到了这个坑,说不定永远也不会去仔细看Block实现的原理~

关键就是这个copy,所以在block中如果引用了(强引用了该block的)类,就一定要注意,检查会不会出现循环引用!

阅读剩下更多

编程

用状态机来实现复杂操作

Context

这次新产品的蓝牙绑定过程有点儿复杂

与炫轮作比较

炫轮只有一个characteristic,不同的操作之间没有顺序关系,都是并列的

新产品有多个characteristic,每个characteristic有不同的意义和不同的指令,每个操作都有先后顺序

Solution

突然想起来,本科的微处理器,还是数字电路课,讲过状态机这个玩意儿.我觉得,可以拿来搞一搞.这样清晰明了,可以省去很多中间变量.

比如

BOOL isConnecting;
BOOL isDiscovering;
BOOL didWriteXXX;
BOOL didReadXXX;
.....

使用了状态机,并且规定好了状态转移的条件,那么,整个系统就一定在这个状态列表中滚.不会出现位置状态,导致程序跑飞.

因为从状态A–>状态B,只有当前条件是xxxx的时候才会实现.这时候不用再去管其他的变量,其他的事件.

使用了状态机,可以轻松地对已有逻辑进行扩充,因为只要拿出,状态转移图来.找到需要添加的地方,就能确定在代码中,有多少地方需要修改,如此一来,就不用在需要该需求的时候,重新阅读已有代码了!

使用了状态机,一般只需要一个记录状态的变量就够了,而且重写这个变量的Setter方法,还可以方便做一些其他的事情:

-(void)setStage:(VKLocatorPairState)stage{
    NSLog(@"pairing status %@",[PublicHelper localizedStringForPairStatus:stage]);
    //发送通知
    //如果是终止状态,发送完通知后,回到原始状态
    [[NSNotificationCenter defaultCenter]postNotificationName:kPairLocatorStatusUpdated object:@(stage)];
    if (stage==1) {  //透露太多了,要被周老板干掉的
        //透露太多了,要被周老板干掉的
    }
    if (stage==1) {  //透露太多了,要被周老板干掉的
        //透露太多了,要被周老板干掉的
    }
    if (stage==1) {  //透露太多了,要被周老板干掉的
        //透露太多了,要被周老板干掉的
    }
}

对,转移状态,和UI可以分开处理,免得以后要界面逻辑,还要去状态转移中找代码.

变量和类名都起得很不要脸啊,小伙子

总之

这一篇,看起来像硬广啊.

再总之

Android端也可以很轻松的做移植.

所以做Android的这个操作,差不多就是在做翻译工作,把Objective-C 翻译为 Java就好了.

阅读剩下更多

编程

ios 命令行单元测试

在开发iOS大项目的时候,由于模块太多,每个功能的模块也太多,在后续测试调试的时候,遇见了bug,就会花比较长的时间去定位bug所在的类或者所在的函数.如果使用了单元测试便可以缩小查找bug的范围.

XCode自带单元测试的命令行工具,可以编写UnitTest文件来自动测试工程,下面来介绍一下它的一些些用法.

首先得确定有xcodebuild命令(跳过,都是iOS开发者,肯定装了XCode咯)

$xcodebuild

肯定没这么简单,假设当前目录是有XCode的工程的,那么这个命令就会执行与在xcode里面点击build一样的效果.

使用xcodebuild运行测试

xcodebuild 命令就是执行了 XCode 里面的build,不过这个命令有一些其他的参数,用法太多这儿就不解释了.总之给他加点儿不同的参数就可以做不同的事情.使用 test 来测试.用-scheme指定操作的scheme.用 -destination 指定target.要在本地 iPhone 5模拟机 “iPhone 5” 测试某个工程的某个测试,使用如下命令来指定目标和架构:

xcodebuild test -project rct6updater.xcodeproj -scheme rct6updaterTest -destination 'platform=iOS ,name=iPhone 5'

如果想对真机操作,可以按照名称或id.比如,我的测试机叫做”VikingWarlock 5”,可以这样来测试代码:

xcodebuild test -workspace xuanwheel/xuanwheel.xcworkspace -scheme xuanwheelTests -destination 'platform=iOS,name=VikingWarlock 5'

测试也可以在 iOS模拟器上运行.使用模拟器可以应对不同的外形因素和操作系统版本.例如:

xcodebuild test -workspace xuanwheel/xuanwheel.xcworkspace -scheme xuanwheelTests -destination 'platform=iOS Simulator,name=iPhone,0S=7.0' -destination 

如果测试失败,xcodebuild 会有红色的 FAIL 提示,修改修改测试的代码,多加一些提醒,这样可以更方便的定位错误点.

等下

之前是-project的,怎么突然使用了-workspace嘞?因为我的工程使用cocoaPods了,所以测试的时候要使用workspace才行.

阅读剩下更多

编程

删除静态库中的静态库~

iOS静态库开发的故事

假设我们要做一个SDK给别人用,有两个选择,framework包,a包.他们有啥区别?framework包里面可以包含图片资源.a包里面只能是纯代码.所以a文件的容量真的很小很小.

万一,我们制作静态库的时候调用了第三方库,在打包的时候,如果不把这些第三方库去掉,会有什么效果呢?

使用我们SDK的人如果也使用了相同的第三方库,就会导致在编译的时候,出现duplicate信号,因为两个相同的类出现了,所以呢,如果一定要使用a包,只能手动的将包内的第三方库给去掉.

OK, 我们来看看删掉一个.a静态库中的某个第三方静态库需要怎么搞

Context

我们做了一个.a静态库,其中包含了

Masonary
JFMinimalNotification
FCFileManager
.....

第一步:

lipo * -thin [platform] dir/new.a

platform是平台,什么x86啊,arm之类的

ar -t dir/new.a

解压出来

cd dir && ar xv new.a

跳转到那个文件夹

rm *MAS*
rm JF*
rm ...

删啊删啊删

cd ..
ar rcs lib_1.a dir/*.o 

重新再打包成lib_1.a

lipo -create ... -output new_clean_static_lib.a

最后生成多平台的静态库


这有什么问题嘛?

嗯,你发布的SDK肯定是多平台的,也就是说,你需要有多少个平台,上面的事情就得做多少次~~

So,我写了一个Python脚本专门来解决这个难过的事情,VKRemove

Function 1

python VKRemove.py -lp xxx.a

返回这个静态库里面包含的平台

This library contains:i386 armv7 x86_64 arm64

Function 2

python VKRemove.py -lb xxx.a

返回这个静态库里面所有的库

...'View+MASAdditions.o', 'ViewController+MASAdditions.o', 'XuanWheelBluetoothManager.o'...

感觉很高端了对吧?

Function 3

python VKRemove.py -i XuanWheelSDK.a -rm FCFileManager HMSegmentedControl- JFMinimalNotification   UIView+Round UIImage+ImageEffects.o MAS

这会生成一个xxx.a.new,它包含了之前所有的平台,并且去掉了指定的所有第三方库.

感觉更高端了吧?

Function 4

python VKRemove.py -i XuanWheelSDK.a -from clear.txt

每次都输入那么长的命令太难过了,不如从文件输入吧~

当然咯,Function 3中的-是干什么用的,可以直接去Github上面看,虽然是英文的~

阅读剩下更多

返回顶部