基于iOS快递查询App的实现

作者: LeeLom | 来源:发表于2016-08-03 14:11 被阅读2967次

    功能介绍


    • 实现提供<b>快递单号</b>和<b>快递公司</b>查询快递轨迹
    • 实现<b>二维码</b>和<b>条形码</b>扫描查询
    • 实现对个人查询记录保存
    • 实现市场常见的国内与国际快递电话的查询与拨打
    • 运行截图


      运行截图

    前期准备


    • 快递api的选择:
      目前提供免费的快递api查询的主要有快递鸟,快递100等。在本文中,我选择了快递鸟作为我的快递api查询接口。主要原因是:快递鸟对常见的快递(顺丰,圆通,中通,韵达等)都提供免费查询,而快递100则需要通过其他方式,自助定制性不强。
      具体注册步骤可以参见官网教程。
    • 快递电话名称信息的获取:
      在App的第三个View中,实现了对常见快递客服电话的查询拨打,这里的信息通过网络爬虫获取。
      快递电话地址
    • 爬虫用到的工具:urllib2+BeautifulSoup
    • 本文我这里为了图省事,直接将上述网页中的源代码保存到代码中存储为html_doc进行处理。
    #-*- coding:utf-8 -*-
    from bs4 import BeautifulSoup
    html_doc="""这是上述网页的源代码"""
    soup = BeautifulSoup(html_doc,"html.parser")
    #用来存储快递名称的数组
    express = []
    #用来存储快递电话号码的数组
    phoneNum = []
    #快递名称都位于标签<h4>中
    #去掉网页中位于快递名称后的“电话”两个字并保存在express中
    for li in soup.find_all('h4'):
       data = str(li.get_text()).replace("电话","")
        express.append(data)
    #快递电话位于标签<b>中
    for li in soup.find_all('b'):
        data = li.get_text()
        phoneNum.append(data)
    expressLen = len(express)
    #输出到控制台上
    for i in range(expressLen):
        print('express'+str(i)+',',end="")
    
    • 处理中遇到的问题:
      • 开始使用Python2.7,中文处理总出问题,编码解码网上也查了很多资料,最后还是放弃专用了3.4,一试就成功;
      • Python2.7 和 Python3.4 可以在电脑上共存,由于mac os中某些系统程序会用到2.7,因此将其保留。在控制台中执行<b>.py</b>程序时,只需要<b>Python3 xxx.py</b>即可。

    程序实现


    代码放在Github上了,欢迎folk,欢迎star
    程序代码

    • 程序示意图


      iOS快递模块.png

    程序界面

    • 查询模块:
      1. 用户输入:快递单号(Required),快递公司(Required),快递备注(Optional,用户在快递历史模块提供备注查看的作用);
      2. 获取用户输入的快递单号和快递公司信息,将请求的参数按照快递鸟官方要求的格式处理(在这里会用到MD5加密以及Base64编码);
      3. 借助第三方库:AFNetworking ,将第二步中处理好的数据POST给快递鸟,快递鸟返回Json数据;
      4. 处理返回的Json数据并且显示物流信息;
      5. 用户点击“保存”按钮,将本单的物流信息保存到plist文件当中实现数据的持久化存储。
      6. 用户还可以使用二维码或条形码扫描功能,实现自动识别快递单号。
        利用AFNetworking POST网络参数
    //网络请求的部分代码
     -(IBAction)expressSearch:(id)sender {
        //1.编写上传参数
    //获取快递公司对应的编号
        NSString* shipperCode = shipperCodeUser;
    //获取快递单号
        NSString* logisticCode = self.expressNum.text;
    //根据快递鸟api请求的要求格式进行数据的处理
    //其中eBusinessID,appKey是快递鸟api提供
    //reqURL:请求的网址,同样是由快递鸟api提供
        NSString* requestData = [NSString stringWithFormat:@"{\"OrderCode\":\"\",\"ShipperCode\":\"%@\",\"LogisticCode\":\"%@\"}",shipperCode,logisticCode];
        NSMutableDictionary* params = [[NSMutableDictionary alloc]init];
        NSString* dataSignTmp = [[NSString alloc]initWithFormat:@"%@%@",requestData,appKey];
    //这里使用到了MD5加密程序,以及Base64编码
        NSString* dataSign = [[MD5 md5String:dataSignTmp] base64String];
        [params setObject:[requestData stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] forKey:@"RequestData"];
        [params setObject:eBusinessID forKey:@"EBusinessID"];
        [params setObject:@"1002" forKey:@"RequestType"];
        [params setObject:[dataSign stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] forKey:@"DataSign"];
        [params setObject:@"2" forKey:@"DataType"];
        //2.上传参数并获得返回值
        AFHTTPSessionManager* manager = [AFHTTPSessionManager manager];
        manager.responseSerializer = [AFHTTPResponseSerializer serializer];
        [manager POST:reqURL parameters:params progress:^(NSProgress * _Nonnull uploadProgress) {
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            NSLog(@"请求成功:%@",responseObject);
            NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseObject options:0 error:nil];
        //3. 获得网络数据赋值给ExpressInfo对象
            NSMutableArray* expressTraces = [[NSMutableArray alloc]init];
            for (NSDictionary* traces in [json objectForKey:@"Traces"]) {
                [expressTraces insertObject:traces atIndex:0];
            }
            NSString* shipperCode = [json objectForKey:@"ShipperCode"];
            NSString* logisticCode = [json objectForKey:@"LogisticCode"];
            NSString* expressForUser = self.expressForUser.text;
            ExpressInfo* express = [[ExpressInfo alloc]initWitfShipperCode:shipperCode andlogisticCode:logisticCode andexpressForUser:expressForUser andexpressTraces:expressTraces];
        //4. 传递数据给ExpresstracesViewController
    //获取的物流数据在ExpressTracesViewController中显示
            ExpressTracesViewController* expressTracesVC = [[ExpressTracesViewController alloc]init];
            expressTracesVC.express = express;
            self.hidesBottomBarWhenPushed = YES;
            [self.navigationController pushViewController:expressTracesVC animated:YES];
            self.hidesBottomBarWhenPushed = NO; 
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            NSLog(@"请求失败:%@",error.description);
        }];
    }
    

    向Plist中读取并写入数据

    //保存数据到plist中的代码
    -(void)saveExpressTraces{
        //1. 获得文件路径
        NSString* path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
        NSString* fileName = [path stringByAppendingPathComponent:@"123.plist"];
        //2. 获得本单快递信息
        NSArray* array = [NSArray arrayWithObjects:shipperCode,logisticCode,expressForUser,expressTraces, nil];
        //3. 获取已经保存的快递信息
        NSMutableArray* arrayCollection = [[NSMutableArray alloc]init];
        NSArray *result = [ NSArray arrayWithContentsOfFile:fileName];
        for (NSArray* obj in result) {
            [arrayCollection addObject:obj];
        }
        //4. 将本单快递信息追加到已有的信息中
        [arrayCollection addObject:array];
        //5. 存储
        [arrayCollection writeToFile:fileName atomically:YES];
        //6. 提示框
        UIAlertView* alert = [[UIAlertView alloc]initWithTitle:@"提示" message:@"保存成功" delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
        [alert show];
    }
    

    选择iOS系统相册图片

    //获取相册照片
        // 1.判断相册是否可以打开
        if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) return;
        // 2. 创建图片选择控制器
        UIImagePickerController *ipc = [[UIImagePickerController alloc] init];
        ipc.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
        // 3.设置代理
        ipc.delegate = self;
        // 4.modal出这个控制器
        [self presentViewController:ipc animated:YES completion:nil];
    
    //实现相关协议
    #pragma mark -------- UIImagePickerControllerDelegate---------
    -(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
    {
        // 1.取出选中的图片
        UIImage *pickImage = info[UIImagePickerControllerOriginalImage];
        NSData *imageData = UIImagePNGRepresentation(pickImage);
        CIImage *ciImage = [CIImage imageWithData:imageData];
        // 2.从选中的图片中读取二维码数据
        // 2.1创建一个探测器
        CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{CIDetectorAccuracy: CIDetectorAccuracyLow}];
        // 2.2利用探测器探测数据
        NSArray *feature = [detector featuresInImage:ciImage];
        // 2.3取出探测到的数据
        for (CIQRCodeFeature *result in feature) {
            NSLog(@"%@",result.messageString);
            NSString *urlStr = result.messageString;
            [[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlStr]];
        }
        // 注意: 如果实现了该方法, 当选中一张图片时系统就不会自动关闭相册控制器
        [picker dismissViewControllerAnimated:YES completion:nil];
    }
    

    利用摄像头捕捉二维码或者条形码

    #pragma mark --------AVCaptureMetadataOutputObjectsDelegate ---------
    -(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
    {
        AVMetadataMachineReadableCodeObject *object = [metadataObjects lastObject];
        if (object == nil) return;
        // 只要扫描到结果就会调用
        self.customLabel.text = object.stringValue;
        NSDictionary* userInfo = [NSDictionary dictionaryWithObject:object.stringValue forKey:@"userInfo"];
        [self clearLayers];
        [self dismissViewControllerAnimated:YES completion:^{
            [[NSNotificationCenter defaultCenter]postNotificationName:@"do" object:self userInfo:userInfo];
        }];
        /*
         将secendView dismissViewControllerAnimated掉,然后自动注册一个名为do的通知
         注册了这个名为的通知,你就可以在任何.m文件里面通过以下代码调用到了:
         NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
         [nc addObserver:self
         selector:@selector(handleColorChange:)
         name:@"do"
         object:nil];
         上面的代码的意思就是,先找到已经注册过的名为do的通知,然后再自动调用handleColorChange去处理
         */
        // 2.对扫描到的二维码进行描边
        AVMetadataMachineReadableCodeObject *obj = (AVMetadataMachineReadableCodeObject *)[self.previewLayer transformedMetadataObjectForMetadataObject:object];
        [self drawLine:obj];
    }
    

    • 快递历史模块:
      1. 获取plist中的物流信息;
      2. 显示到tableview中。
    //获取plist文件路径
        NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    //得到123.plist文件
        NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
    //获取123.plist文件中的返回值
        NSArray *result = [ NSArray arrayWithContentsOfFile:fileName];
    //将返回值复制给(NSMutableArray* )expressHistory
        expressHistory = [NSMutableArray arrayWithArray:result];
    

    • 快递电话模块
      1. 新建一个快递模型Class:ExpressPhoneNum.其中有两个成员变量,快递名称和快递电话;
      2. 将获取到的快递信息存储为ExpressPhoneNum类型
      3. 将数据显示到tableview
      4. 点击某行实现拨打电话的功能
    //点击行打电话
    -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    //获取点击的Section
        NSArray* expressXX = self.expressArray[indexPath.section];
    //获取点击的快递信息
        ExpressPhoneNum* expressXXX = expressXX[indexPath.row];
        NSString* expressPhoneNumber = expressXXX.expressNum;
        NSMutableString* str = [[NSMutableString alloc]initWithFormat:@"tel:%@",expressPhoneNumber];
    //实现拨号功能
        UIWebView* callWebView = [[UIWebView alloc]init];
        [callWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:str]]];
        [self.view addSubview:callWebView];
        NSLog(@"打电话啊!");
    }
    

    下一步的工作


    • 界面美化:程序当中所有的控件都是系统默认的,未做任何修改;
    • 程序整体功能执行没问题,但存在个别bug,有待优化

    参考资料


    相关文章

      网友评论

      • 1d9c03a615a3:谢谢楼主,文章写的不错,就是ios9之后 [params setObject:[requestData stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] forKey:@"RequestData"];中的stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding要替换
        1d9c03a615a3:@LeeLom 不敢不敢,你您的文章帮助了我,谢谢啦
        LeeLom:@仰望星空的摆渡人 多谢指教。:smile:
      • 庚庚庚:楼主,pod安装完成之后,一直提示Apple Mach-O Linker(Id) Error
        Linker command failed with exit code 1的错误 怎么解决
        在stackoverflow上搜索的答案并不适用
      • Ko_Neko:博主你好,下载了你的demo。编译报错:
        diff: /../Podfile.lock: No such file or directory
        diff: /Manifest.lock: No such file or directory
        error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.

        但是我发现您的包内并没有包含pod文件。不知道是怎么回事呢?
        卖太妃糖的古尔丹:兄弟,他的问题你解决了吗,怎么解决的
        卖太妃糖的古尔丹:@LeeLom 没看到podfile文件啊,是要自己导入第三方库吗
        LeeLom:Podfile中的文件:你可以自己添加以下。
        platform:ios,'8.0'
        pod 'AFNetworking', '~> 3.1.0'
      • 加了个油:楼主,提示 A build only device cannot be used to run this target 应该怎样解决呢
        加了个油:@LeeLom 是的,打开后,就出现这个问题了,没看到有模拟器
        LeeLom:@加了个油 stackoverflow上有很多解决办法,你可以参考一下
        LeeLom:@加了个油 你是下载我这个demo运行出现的问题吗?

      本文标题:基于iOS快递查询App的实现

      本文链接:https://www.haomeiwen.com/subject/ecpqsttx.html