iOS 重构:UITableView信息录入页高效开发[直接减少30%工作量]

[2020/0706/14:18] 更新:
在 Swift 中通过 switch 区别 cell,工作效率提升至 30%:
1.高复用(所有录入界面均可使用同一套区分逻辑),详情页大部分代码都可复用;
2.高扩展(因为通过cell 类名字符串区别,所以增, 删, 改(顺序位置)都不会影响到已经实现的 cell 逻辑,拥有极强的扩展性);

思路
[通过区分 cell,构建不同的显示, 基于KVC的非无限数据界面通用化.]

    var dataModel = CouponRechargeRecordModel()
        
    lazy var list: [[[String]]] = {
        var array: [[[String]]] = [
            [
            ["*车场" + kBlankFour, "UITableViewCell", "50.0", "请选择", "parkname", ],
            ["*商户" + kBlankFour, "UITableViewCell", "50.0", "请选择", "name", ],
            ["*缴费金额" + kBlankTwo, "UITableViewCellTextField", "50.0", "请输入", "charge_money", ],
            ["*券类别" + kBlankThree, "UITableViewCellSegment", "50.0", "", "categoryStr", "预定义,自定义"],
            ["*券类型" + kBlankThree, "UITableViewCell", "50.0", "请选择", "couponTypeValue", ],
            ["*面额" + kBlankFour, "UITableViewCellTextField", "50.0", "请输入", "faceValue", ],
            ["*张数" + kBlankFour, "UITableViewCellStep", "50.0", "请输入", "count", ],
            ["有效时间" + kBlankTwo, "UITableViewCellDateRange", "50.0", "", "validbtime,validetime", ],
            ["有效时段" + kBlankTwo, "UITableViewCellDateRange", "50.0", "", "btime,etime", ],
            ["备注" + kBlankFour, "UITableViewCellTextField", "50.0", "请输入", "memo", ],
            ],
        ]
        return array
    }()

extension IOPMerchantRechargeEntryController: UITableViewDataSource, UITableViewDelegate{
    //    MARK: - tableView
    func numberOfSections(in tableView: UITableView) -> Int {
        return list.count;
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let sections = list[section]
        return sections.count;
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if dataModel.categoryStr == "1" {
            let sections = list[indexPath.section]
            let itemList = sections[indexPath.row]
            if couponFixTypeItems.contains(itemList[0]) {
                return 0.0
            }
        }
        return tableView.rowHeight
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let sections = list[indexPath.section]
        let itemList = sections[indexPath.row]
        let value0 = itemList[0]
        let value2 = itemList[2]
        let value3 = itemList[3]
        let value4 = itemList[4]
        
        switch itemList[1] {
        case "UITableViewCellTextField":
            let cell = UITableViewCellTextField.cellWithTableView(tableView)
            cell.labelLeft.font = UIFont.systemFont(ofSize: 14)
            cell.labelLeft.textColor = UIColor.textColor3
            cell.isHidden = value2.cgFloatValue <= 0.0
            cell.hasAsterisk = value0.contains("*")
            
            cell.textfield.rightViewMode = .never;
            cell.textfield.textAlignment = .left

            cell.labelLeft.text = value0
            cell.textfield.placeholder = value3
            cell.textfield.text = dataModel.valueText(forKeyPath: value4, defalut: "")
            cell.block { (textField) in
                self.dataModel.setValue(textField.text ?? "", forKeyPath: value4)
//              DDLog("%@_%@_", value4, self.dataModel.valueText(forKeyPath: value4))
            }
            
            if ["charge_money", "faceValue"].contains(value4) {
                cell.textfield.textAlignment = .right
                
                if value4 == "charge_money" {
                     cell.textfield.asoryView(true, text: " 元")

                } else  if value4 == "faceValue" {
                    let unit = CouponRecordModel.getCouponUnit(self.dataModel.couponTypeValue)
                    cell.textfield.asoryView(true, text: " " + unit)
                }
            }
//            cell.textfield.getViewLayer()
//            cell.getViewLayer()
            return cell
            
        case "UITableViewCell":
            let cell = UITableViewCell.dequeueReusableCell(tableView, identifier: "UITableViewCellValue1", style: .value1)
            cell.textLabel?.font = UIFont.systemFont(ofSize: 15)
            cell.textLabel?.textColor = UIColor.textColor3;

            cell.detailTextLabel?.font = UIFont.systemFont(ofSize: 15)
            cell.detailTextLabel?.textColor = UIColor.theme
            cell.accessoryType = .disclosureIndicator;
            
            cell.textLabel?.text = value0
            cell.detailTextLabel?.text = "上传"
                        
//            let fileTitles: [String] = value3.components(separatedBy: ",")
//            cell.detailTextLabel?.text = fileTitles.first!

            let hasAsterisk = value0.contains("*")
            if hasAsterisk {
                cell.textLabel?.attributedText = cell.textLabel!.text!.toAsterisk(cell.textLabel!.textColor, font: cell.textLabel!.font.pointSize)
            }
            
            let result = dataModel.valueText(forKeyPath: value4, defalut: "")
            if value4 == "couponTypeValue" {
                cell.detailTextLabel?.text = CouponRecordModel.getCouponDes(result, defaultValue: "请选择")
            } else if value4 == "name" {
                cell.detailTextLabel?.text = dataModel.clt_id == 0 ? value3 : result
            } else {
                cell.detailTextLabel?.text = result
            }
                        
//            cell.getViewLayer()
            return cell;
            
        case "UITableViewCellSegment":
            let cell = UITableViewCellSegment.dequeueReusableCell(tableView)
            cell.labelLeft.font = UIFont.systemFont(ofSize: 14)
            cell.labelLeft.textColor = UIColor.textColor3
            cell.isHidden = itemList[2].cgFloatValue <= 0.0
            cell.hasAsterisk = value0.contains("*")

            cell.labelLeft.text = value0
            cell.segmentCtl.items = (itemList.last! as NSString).components(separatedBy: ",")
            let index = dataModel.valueText(forKeyPath: value4, defalut: "2") == "2" ? 0 : 1
            cell.segmentCtl.selectedSegmentIndex = index
//            cell.segmentCtl.addTarget(self, action: #selector(handleActionSender(_:)), for: .valueChanged)
            cell.segmentCtl.addActionHandler({ (control) in
                guard let sender = control as? UISegmentedControl else { return }
                let value: String = sender.selectedSegmentIndex == 0 ? "2" : "1"
                self.dataModel.setValue(value, forKeyPath: value4)
                tableView.reloadData()
//                DDLog("%@_%@_%@, sender.selectedSegmentIndex, value4, value)

            }, for: .valueChanged)
//            cell.getViewLayer()
            return cell
        
        case "UITableViewCellStep":
            let cell = UITableViewCellStep.dequeueReusableCell(tableView)
            cell.labelLeft.font = UIFont.systemFont(ofSize: 14)
            cell.labelLeft.textColor = UIColor.textColor3
            cell.isHidden = value2.cgFloatValue <= 0.0
            cell.hasAsterisk = value0.contains("*")
            
            cell.labelLeft.text = value0
            cell.ppBtn.minValue = 0
            cell.ppBtn.maxValue = 10
            //            cell.ppBtn.currentNumber = value4
            cell.ppBtn.currentNumber = dataModel.valueText(forKeyPath: value4, defalut: "1")
            cell.ppBtn.numberResult { (num) in
//                    DDLog(num)
                self.dataModel.setValue(num, forKey: value4)
            }
//            cell.textfield.textAlignment = .right
            
            if dataModel.categoryStr == "1" {
                cell.isHidden = couponFixTypeItems.contains(itemList[0])
            }
//            cell.getViewLayer()
            return cell
            
        case "UITableViewCellDateRange":
            let cell = UITableViewCellDateRange.dequeueReusableCell(tableView)
            cell.dateRangeView.labTitle.font = UIFont.systemFont(ofSize: 14)
            cell.dateRangeView.labTitle.textAlignment = .left
            cell.dateRangeView.labTitle.textColor = UIColor.textColor3
            cell.isHidden = value2.cgFloatValue <= 0.0
            cell.hasAsterisk = value0.contains("*")
            
            let datePickerMode: UIDatePicker.Mode = value4.contains("valid") ? .date : .time
            cell.dateRangeView.datePicker.datePicker.datePickerMode = datePickerMode

            cell.dateRangeView.isEmptyDate = true
            cell.dateRangeView.isFuture = true
            cell.dateRangeView.labTitle.text = value0
            let dateTimeKeys = (itemList.last! as NSString).components(separatedBy: ",")
            
            let beginTime: String = dataModel.valueText(forKeyPath: dateTimeKeys.first!)
            let endTime: String = dataModel.valueText(forKeyPath: dateTimeKeys.last!)
            if beginTime.count == 19 && endTime.count == 19 {
                cell.dateRangeView.beginDate = DateFormatter.dateFromString(beginTime)
                cell.dateRangeView.endDate = DateFormatter.dateFromString(endTime)
            }

            cell.dateRangeView.block { (dateRangeView) in
                DDLog(dateRangeView.beginTime, dateRangeView.endTime)
                if dateRangeView.endTime.isNewer(value: dateRangeView.beginTime) == false {
                    IOPProgressHUD.showText("结束时间必须大于开始时间")
                    return
                }
                
                if value4 == "btime,etime" {
                    let btime = "\((dateRangeView.beginTime as NSString).substring(with: NSMakeRange(11, 5)))"
                    let etime = "\((dateRangeView.endTime as NSString).substring(with: NSMakeRange(11, 5)))"
                    if etime.isNewer(value: btime) == false {
                        IOPProgressHUD.showText("结束时段必须大于开始时段")
                        return
                    }
                }
                
                self.dataModel.setValue(dateRangeView.beginTime, forKey: dateTimeKeys.first!)
                self.dataModel.setValue(dateRangeView.endTime, forKey: dateTimeKeys.last!)
            }
            
            if dataModel.categoryStr == "1" {
                cell.isHidden = couponFixTypeItems.contains(itemList[0])
            }
//            cell.getViewLayer()
            return cell
        default:
            break
        }
        let cell = UITableViewCellZero.cellWithTableView(tableView)
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let sections = list[indexPath.section]
        let itemList = sections[indexPath.row]
        switch itemList[4] {
        case "couponTypeValue":
            self.pickView.itemList = self.shopModel.getCouponList(self.dataModel.categoryStr)
            pickView.block({ (view:NNPickListView, indexP:IndexPath) in
                DDLog(indexP.string)
                guard let items = view.itemList as [[String]]?,
                let cellItem = items[indexP.row] as [String]?
                    else { return }
//                self.dataModel.couponType = CouponType(rawValue: value)!
                self.dataModel.couponTypeValue = cellItem[1]
                self.dataModel.faceValue = self.dataModel.couponTypeValue == "2" ? 1 : 0
                tableView.reloadRowList([indexPath.row, indexPath.row+1], section: indexPath.section)
            })
            pickView.show()
            break

        case "parkname":
            present(parkChooseNaVC, animated: true, completion: nil)
//            navigationController?.pushViewController(parkChooseVC, animated: true)
            break

        case "name":
            present(merchantChooseNaVC, animated: true, completion: nil)
//            navigationController?.pushViewController(merchantChooseVC, animated: true)

            break
        default:
            break
        }
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 10.01;
    }
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        return UILabel();
    }
    
    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return 0.01;
    }
    
    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        return UILabel();
    }
}

步骤 一:因为使用的 KVC,所以要通过 hook处理值为空的情况:

import UIKit

@objc public extension NSObject{
    /// 实例方法替换
    @discardableResult
    static func hookInstanceMethod(of origSel: Selector, with replSel: Selector) -> Bool {
        let clz: AnyClass = classForCoder();

        guard let oriMethod = class_getInstanceMethod(clz, origSel) as Method?,
        let repMethod = class_getInstanceMethod(clz, replSel) as Method?
        else {
            print("Swizzling Method(s) not found while swizzling class \(NSStringFromClass(classForCoder())).")
            return false
        }
        
        //在进行 Swizzling 的时候,需要用 class_addMethod 先进行判断一下原有类中是否有要替换方法的实现
        let didAddMethod: Bool = class_addMethod(clz, origSel, method_getImplementation(repMethod), method_getTypeEncoding(repMethod))
        //如果 class_addMethod 返回 yes,说明当前类中没有要替换方法的实现,所以需要在父类中查找,这时候就用到 method_getImplemetation 去获取 class_getInstanceMethod 里面的方法实现,然后再进行 class_replaceMethod 来实现 Swizzing
        if didAddMethod {
            class_replaceMethod(clz, replSel, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod))
        } else {
            method_exchangeImplementations(oriMethod, repMethod)
        }
        return true;
    }
}

@objc public extension NSObject{
    class func initializeMethod() {
        if self != NSObject.self {
            return
        }

        let onceToken = "Hook_\(NSStringFromClass(classForCoder()))";
        DispatchQueue.once(token: onceToken) {
            let oriSel = #selector(self.setValue(_:forUndefinedKey:))
            let repSel = #selector(self.hook_setValue(_:forUndefinedKey:))
            hookInstanceMethod(of: oriSel, with: repSel);
            
            let oriSel0 = #selector(self.value(forUndefinedKey:))
            let repSel0 = #selector(self.hook_value(forUndefinedKey:))
            hookInstanceMethod(of: oriSel0, with: repSel0);
            
            let oriSel1 = #selector(self.setNilValueForKey(_:))
            let repSel1 = #selector(self.hook_setNilValueForKey(_:))
            hookInstanceMethod(of: oriSel1, with: repSel1);
            
            let oriSel2 = #selector(self.setValuesForKeys(_:))
            let repSel2 = #selector(self.hook_setValuesForKeys(_:))
            hookInstanceMethod(of: oriSel2, with: repSel2);
        }
        
    }
    
    private func hook_setValue(_ value: Any?, forUndefinedKey key: String){
        print("setValue: forUndefinedKey:, 未知键Key: \(key)");
        
    }
    
    private func hook_value(forUndefinedKey key: String) -> Any?{
        print("valueForUndefinedKey:, 未知键: \(key)");
        return nil;
    }
    
    private func hook_setNilValueForKey(_ key: String){
        print("Invoke setNilValueForKey:, 不能给非指针对象(如NSInteger)赋值 nil");
        return;//给一个非指针对象(如NSInteger)赋值 nil, 直接忽略
    }
    
    private func hook_setValuesForKeys(_ keyedValues: [String : Any]) {
        for (key, value) in keyedValues {
//            DDLog(key, value);
            if value is Int || value is CGFloat || value is Double {
                self.setValue("\(value)", forKey: key)
            } else {
                self.setValue(value, forKey: key)
            }
        }
    }
}

步骤 二:调用 hook 方法:

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        NSObject.initializeMethod()
        ...
    }
}

(非 demo, 是项目中相关代码, 这是重型武器, 启动慢, 威力大.)
github.swift

你可能感兴趣的:(iOS 重构:UITableView信息录入页高效开发[直接减少30%工作量])