Category: iOS

  • [Mobile – Flutter] Slider trong Flutter

    [Mobile – Flutter] Slider trong Flutter

    Bản thân Flutter là một framework khá hoàn thiện đến mức việc tạo giao diện với Flutter trở nên dễ dàng và nhanh chóng đến mức bất ngờ! Điều này khiến lập trình viên Flutter cảm thấy việc lập trình mobile quá dễ (trừ khi họ là người đã từng nếm trái đắng từ khi code native). Mình cũng thấy dễ cho đến khi nhìn thấy giao diện ứa nước mắt mà designer gửi (đôi khi là kèm theo video animation siêu xịn…).

    Thế bạn đã trang bị cho mình gì để ứng phó trước những design đẹp (hoặc k) nào? Bạn đã biết custom Slider trong Flutter chưa? Hay chỉ là những bé slider cũ mèn như này này này:

    mặc định
    range slider
    cupertino *hình ảnh chụp từ video youtube nên hơi mờ

    Nhưng khi nhận được design thì trông như này

    như này nè
    rồi như này
    trông cũng dễ?

    Chắc ai nhận design cũng sẽ nghĩ: trông dễ phết, khó thì dùng thư viện (nhiều thư viện Slider quá mà). Có thể kể ra một số thư viện cho ai muốn dùng nè:

    Đọc đến đây thì ai thích dùng thư viện có thể dừng lại và tham khảo 3 thư viện này. Đôi khi một số lại thấy thư viện thì quá là nặng thêm nhiều thứ lắt nhắt, rồi cho customize đủ thứ mà cái mình cần thì không có? Hoặc bạn muốn tự mình thử sức làm những slider này, thì tiếp tục đọc cách custom slider nào :3

    Đầu tiên, chúng ta sẽ tìm hiểu về các thành phần của 1 slider và tên gọi của nó trong Flutter với thiết kế theo Material: (đoạn này cop trên document lười dịch nên nhờ bạn đọc tạm nhé)

    • The “thumb”, which is a shape that slides horizontally when the user drags it.
    • The “track”, which is the line that the slider thumb slides along.
    • The “value indicator”, which is a shape that pops up when the user is dragging the thumb to indicate the value being selected.
    • The “active” side of the slider is the side between the thumb and the minimum value.
    • The “inactive” side of the slider is the side between the thumb and the maximum value.
    • The “tick marks”, which are regularly spaced marks that are drawn when using discrete divisions.
    • The “value indicator”, which appears when the user is dragging the thumb to indicate the value being selected.
    • The “overlay”, which appears around the thumb, and is shown when the thumb is pressed, focused, or hovered. It is painted underneath the thumb, so it must extend beyond the bounds of the thumb itself to actually be visible.

    Để thiết kế ra một Slider thì đầu tiên chúng ta sẽ xem đến các widget trực tiếp để tạo ra slider: Slider, RangeSlider, and CupertinoSlider(Flutter Widget of the Week) – YouTube. Tuy nhiên khi xem document (ờ lại là document, document của Flutter là tài liệu hữu ích nhất mà dev Flutter có thể tìm được bên cạnh source code open của nó) thì bạn phát hiện ra nó không có nhiều thứ liên quan đến giao diện cho lắm, ngoài việc cho phép đổi tí màu. Bạn bắt đầu tuyệt vọng.

    một constructor của slider
    có gì đó

    và rồi cuối document, họ bảo, vẻ bề ngoài của nó thì dùng SliderThemeData từ widget SliderTheme hoặc một cái theme nào đó cha ông của nó mà có định nghĩa sliderTheme. Yeah XD

    Bạn tiếp tục đi đến wiki của SliderThemeData xem tùy chỉnh được gì, ở đây nhé SliderThemeData class – material library – Dart API (flutter.dev). Có thể chỉnh được kha khá, hình tròn hình chữ nhật, bo góc (hoặc k), … bạn có thể dừng lại, đọc, và thử xem bạn có thể custom như nào (ảnh phía trên là 1 slider đã được 1 tác giả khác tạo ra nhờ tùy chỉnh các thuộc tính này).

    Để rồi nhận ra rằng: nó không giống mấy cái design nhận được xíu nào 🙁

    hình ảnh slider trong blog cuối bài

    Thế làm sao để custom chúng nó như này? Hãy xem Flutter Team đã tạo ra Slider như nào đã nhé :3

    ồ là một file hơn 3000 dòng code và rất nhiều tác giả

    Họ đã tạo ra RoundedRectSliderTrackShape, RoundSliderThumbShape, PaddleSliderValueIndicatorShape, PaddleSliderValueIndicatorShape, RoundSliderTickMarkShape,… và document cũng nói là:

    The thumb, track, tick marks, value indicator, and overlay can be customized by creating subclasses of SliderTrackShapeSliderComponentShape, and/or SliderTickMarkShape. See RoundSliderThumbShapeRectangularSliderTrackShapeRoundSliderTickMarkShapeRectangularSliderValueIndicatorShape, and RoundSliderOverlayShape for examples.

    Document

    Nên cứ nghe theo thôi ha, họ bảo code mẫu rồi đấy, muốn custom thì xem mà học :3 họ bảo kế thừa mấy cái có sẵn này đi mà custom, nhưng custom sao thì họ không nói 🙁 nên t ở đây để giúp bạn đây hehe. Không dài dòng nữa, bắt đầu thôi :3

    Khi bạn extend các lớp kể ở trên, bạn sẽ được trình gợi ý yêu cầu override hàm paint, đây là hàm để vẽ nên chúng nó. Trong hàm paint có một số thứ bạn cần chú ý:
    – Đầu tiên là PaintingContext context, ở đây bạn sẽ lấy được canvas ra và vẽ những gì bạn cần (context.canvas)
    – Tiếp theo là center với Thumb và parentBox với Track, bạn sẽ tìm được những điểm hữu dụng để vẽ
    – Tiếp theo là sliderTheme, từ đó bạn có thể lấy ra các thuộc tính khác mà bạn có thể truyền vào theme của slider (màu active, inactive…)
    – Cuối cùng, mn luôn luôn có thể tham khảo code open của Flutter xem họ làm như nào với các shape mặc định, hoặc tham khảo code của mình sau đây :3

    Đây là code để tạo ra chiếc slider này

    Chụp từ màn hình điện thoại
    import 'package:flutter/material.dart';
    
    class CustomSlider extends StatelessWidget {
      final double max, min, value;
      final void Function(double) onValueChange;
    
      const CustomSlider({
        required this.min,
        required this.max,
        required this.value,
        required this.onValueChange,
        Key? key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Container(
          decoration: BoxDecoration(
            border: Border.all(color: Colors.grey),
            borderRadius: BorderRadius.circular(10),
          ),
          padding: const EdgeInsets.fromLTRB(4, 16, 4, 16),
          child: Row(
            children: [
              Text(min.toInt().toString()),
              const SizedBox(width: 6),
              Expanded(
                child: SliderTheme(
                  data: SliderTheme.of(context).copyWith(
                    trackHeight: 14.0,
                    trackShape: CustomTrackShape(),
                    activeTrackColor: const Color(0xFF219653),
                    inactiveTrackColor: const Color(0xFFF1E1E1),
                    thumbShape: CustomSliderMarkShape(sliderValue: value, tickMarkRadius: 16),
                    thumbColor: const Color(0xFF219653),
                    // overlayColor: Color(0xFF219653).withOpacity(0.5),
                    // overlayShape: RoundSliderOverlayShape(overlayRadius: 24),
                    overlayShape: SliderComponentShape.noOverlay,
                    showValueIndicator: ShowValueIndicator.always,
                    valueIndicatorShape: const RectangularSliderValueIndicatorShape(),
                    valueIndicatorColor: Colors.black,
                    valueIndicatorTextStyle: const TextStyle(
                      color: Colors.white,
                      fontSize: 16.0,
                    ),
                  ),
                  child: Slider(
                    min: min,
                    max: max,
                    value: value,
                    label: '${value.round()}',
                    onChanged: onValueChange,
                  ),
                ),
              ),
              const SizedBox(width: 6),
              Text(max.toInt().toString()),
            ],
          ),
        );
      }
    }
    
    class CustomSliderMarkShape extends SliderComponentShape {
      final double tickMarkRadius;
      final double sliderValue;
    
      CustomSliderMarkShape({
        required this.tickMarkRadius,
        required this.sliderValue,
      });
    
      @override
      Size getPreferredSize(bool isEnabled, bool isDiscrete) {
        return Size(tickMarkRadius, tickMarkRadius);
      }
    
      @override
      void paint(
        PaintingContext context,
        Offset center, {
        required Animation<double> activationAnimation,
        required Animation<double> enableAnimation,
        required bool isDiscrete,
        required TextPainter labelPainter,
        required RenderBox parentBox,
        required SliderThemeData sliderTheme,
        required TextDirection textDirection,
        required double value,
        required double textScaleFactor,
        required Size sizeWithOverflow,
      }) {
        final Canvas canvas = context.canvas;
    
        canvas.drawRRect(
          RRect.fromRectAndRadius(
            Rect.fromCenter(center: center, width: 36, height: 24), const Radius.circular(16)),
          Paint()..color = Colors.white,
        );
    
        canvas.drawRRect(
          RRect.fromRectAndRadius(
            Rect.fromCenter(center: center, width: 36, height: 24), const Radius.circular(16)),
          Paint()..color = Colors.black
            ..style = PaintingStyle.stroke
            ..strokeWidth = 2,
        );
    
        TextSpan span = TextSpan(
          style: TextStyle(
            fontSize: tickMarkRadius * 0.9,
            fontWeight: FontWeight.w700,
            color: Colors.black
          ),
          text: sliderValue.round().toString(),
        );
    
        TextPainter tp = TextPainter(
          text: span,
          textAlign: TextAlign.center,
          textDirection: TextDirection.ltr,
        );
    
        tp.layout();
    
        Offset textCenter = Offset(
          center.dx - (tp.width / 2),
          center.dy - (tp.height / 2),
        );
        tp.paint(canvas, textCenter);
      }
    }
    
    class CustomTrackShape extends RoundedRectSliderTrackShape {
      @override
      Rect getPreferredRect({
        required RenderBox parentBox,
        Offset offset = Offset.zero,
        required SliderThemeData sliderTheme,
        bool isEnabled = false,
        bool isDiscrete = false,
      }) {
        final double trackHeight = sliderTheme.trackHeight ?? 8;
        final double trackLeft = offset.dx;
        final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
        final double trackWidth = parentBox.size.width;
        return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
      }
    }
    // sử dụng
    CustomSlider(
      max: 100,
      min: 10,
      onValueChange: (_) {  },
      value: 30,
    ),


    Mình đã custom cái slider này từ rất lâu rồi (nên code của nó mình không chắc là ổn lắm).

    Và lí do mình viết bài này chủ yếu để nhớ kĩ hơn và muốn chia sẻ với mọi người khi mà chiều nay mình đã custom ra mấy cái extend SliderComponentShape, rồi ghép vào SliderTheme mà reload restart hoài UI không nhận :'( Mình rất bối rối muốn ném máy ra cửa sổ thì nhận ra nếu là RangeSlider thì cần extend RangeSliderTrackShape, RangeSliderThumbShape… (thêm Range cơ). Hóa ra do mình không đọc kĩ (tiện muốn kể chuyện InteractiveViewer ghê mà ai muốn nghe ib nhé :v)

    range slider sau khi custom
    import 'dart:math';
    
    import 'package:flutter/material.dart';
    import 'dart:ui' as ui;
    
    class CustomRangeSliderTrack extends RangeSliderTrackShape {
      const CustomRangeSliderTrack();
    
      @override
      Rect getPreferredRect({
        required RenderBox parentBox,
        Offset offset = Offset.zero,
        required SliderThemeData sliderTheme,
        bool isEnabled = false,
        bool isDiscrete = false,
      }) {
        final double overlayWidth = sliderTheme.overlayShape!.getPreferredSize(isEnabled, isDiscrete).width;
        final double trackHeight = sliderTheme.trackHeight!;
    
        final double trackLeft = offset.dx + overlayWidth / 2;
        final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
        final double trackRight = trackLeft + parentBox.size.width - overlayWidth;
        final double trackBottom = trackTop + trackHeight;
        // If the parentBox'size less than slider's size the trackRight will be less than trackLeft, so switch them.
        return Rect.fromLTRB(min(trackLeft, trackRight), trackTop, max(trackLeft, trackRight), trackBottom);
      }
    
      @override
      void paint(
        PaintingContext context,
        Offset offset, {
        required RenderBox parentBox,
        required SliderThemeData sliderTheme,
        required Animation<double> enableAnimation,
        required Offset startThumbCenter,
        required Offset endThumbCenter,
        bool isEnabled = false,
        bool isDiscrete = false,
        required TextDirection textDirection,
        double additionalActiveTrackHeight = 2,
      }) {
        final ColorTween inactiveTrackColorTween = ColorTween(
          begin: sliderTheme.disabledInactiveTrackColor,
          end: sliderTheme.inactiveTrackColor,
        );
    
        final Rect trackRect = getPreferredRect(
          parentBox: parentBox,
          offset: offset,
          sliderTheme: sliderTheme,
          isEnabled: isEnabled,
          isDiscrete: isDiscrete,
        );
    
        final Paint activePaint = Paint()
          ..shader = ui.Gradient.linear(
            Offset(0, trackRect.top),
            Offset(0, trackRect.bottom),
            [
              Color(0xFF4BC65E),
              Color(0xFF379A46),
            ],
          );
        final Paint inactivePaint = Paint()
          ..color = inactiveTrackColorTween.evaluate(enableAnimation)!;
        final Size thumbSize = sliderTheme.rangeThumbShape!.getPreferredSize(isEnabled, isDiscrete);
        final double thumbRadius = thumbSize.width / 2;
        assert(thumbRadius > 0);
    
        final Radius trackRadius = Radius.circular(trackRect.height / 2);
    
        context.canvas.drawRRect(
          RRect.fromLTRBAndCorners(
            trackRect.left,
            trackRect.top,
            startThumbCenter.dx,
            trackRect.bottom,
            topLeft: trackRadius,
            bottomLeft: trackRadius,
          ),
          inactivePaint,
        );
        context.canvas.drawRect(
          Rect.fromLTRB(
            startThumbCenter.dx,
            trackRect.top - (additionalActiveTrackHeight / 2),
            endThumbCenter.dx,
            trackRect.bottom + (additionalActiveTrackHeight / 2),
          ),
          activePaint,
        );
        context.canvas.drawRRect(
          RRect.fromLTRBAndCorners(
            endThumbCenter.dx,
            trackRect.top,
            trackRect.right,
            trackRect.bottom,
            topRight: trackRadius,
            bottomRight: trackRadius,
          ),
          inactivePaint,
        );
      }
    }
    
    class CustomRangeSliderThumb extends RangeSliderThumbShape {
      const CustomRangeSliderThumb();
      
      @override
      Size getPreferredSize(bool isEnabled, bool isDiscrete) {
        return Size.fromRadius(20);
      }
      
      @override
      void paint(PaintingContext context,
        Offset center, {
        required Animation<double> activationAnimation, 
        required Animation<double> enableAnimation, 
        bool isDiscrete = false,
        bool isEnabled = false,
        bool? isOnTop,
        required SliderThemeData sliderTheme,
        TextDirection? textDirection,
        Thumb? thumb,
        bool? isPressed,
      }) {
        final Canvas canvas = context.canvas;
        final outerPaint = Paint()
          ..style = PaintingStyle.fill
          ..shader = ui.Gradient.linear(
            Offset(0, 0),
            Offset(0, 30),
            [
              Color(0xFF4BC65E),
              Color(0xFF379A46),
            ],
          );
        canvas.drawCircle(center, 10, outerPaint);
        canvas.drawCircle(center, 5, Paint()..color = Colors.white);
      }
    }
    // dùng như này để có 1 range slider như ý
    // có thể custom thêm gì đó tùy ý nha
    SliderTheme(
      data: SliderThemeData(
        trackHeight: 4,
        rangeTrackShape: CustomRangeSliderTrack(),
        rangeThumbShape: CustomRangeSliderThumb(),
      ),
      child: RangeSlider(
        values: priceRange,
        min: minPrice,
        max: maxPrice,
        onChanged: (value) {
          setState(() {
            priceRange = value;
          });
        },
      ),
    ),

    Tham khảo: Flutter Slider widgets: A deep dive with examples – LogRocket Blog

  • Highlight text of UILabel in Swift

    Highlight text of UILabel in Swift

    Hi mọi người, mấy năm trước mình có làm một dự án mobile về mảng Logistics, trong ứng dụng thường sử dụng khá nhiều các text, một số từ được Highlight text đi kèm với các action tương tự như là một button nhằm mục đích gây sự chú ý với người dùng. Để làm được việc này chúng ta cần xác định được vị trí text cần Highlight để thực hiện thay đổi UI cho đoạn text đó gắn action cho nó. Mình thấy đây là một tính năng khá thú vị và sẽ gặp nhiều trong các dự án sắp tới của các bạn. Vậy nên mình xin chia sẻ cách làm của mình như sau.

    Cách Highlight text

    Về lý thuyết để highlight text trong swift thì chúng ta cần l xác định vị trí(position) và độ dài(lenght) của text cần highlight sau đó thực hiện thay đổi thuộc tính của đoạn text đó bằng NSAttributedString.

    Việc bắt sư kiện khi chúng ta tương tác với Highlight text cũng phải tìm vị trí và độ dài của đoạn text, sau đó chúng ta dựa vào điểm người dùng bấm vào trên Label để xác định xem người dùng có bấm đúng vị trí text được highlight hay không.

    1. Cách thực hiện Highlight text

    Đầu tiên chúng ta sẽ tạo một func trong extension của UILabel như sau:

    extension UILabel {
        func highlightText(_ text: String, highlightColor: UIColor, in mainText: String) {
            // chuyển mainText sang NSMutableAttributedString để xử lý tìm range của highlight text
            let highlightAttributedString = NSMutableAttributedString(string: mainText)
            // xác định vị trí, độ dài của text cần highlight
            let range = (mainText as NSString).range(of: text)
            // thêm thuộc tính cho đoạn text cần highlight
            highlightAttributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: highlightColor, range: range)
            // gán giá trị vào label
            self.attributedText = highlightAttributedString
        }
    }

    Khi sử dụng chúng ta sẽ làm như sau:

    Trường hợp này mình sẽ biến chữ “DaoNM2” trong “Good evening! DaoNM2” thành màu cam như sau

    lbInfo.highlightText("DaoNM2", highlightColor: .red, in: "Good evening! DaoNM2")
    Kết quả

    2. Bắt sự kiện và thực hiện hành động khi bấm vào text được Highlight

    Ở mục 1 mình đã hướng dẫn cách thực hiện đổi màu text của 1 đoạn text trong UILabel, vậy để bắt sự kiện khi chúng ta bấm vào thì làm cách nào?

    Đầu tiên chúng ta sẽ tạo một func trong extension của UITapGestureRecognizer, nhằm mục đích xử lí điểm chạm của người dùng và xác định xem có đúng vị trí của text đã được Highlight hay không như sau:

    extension UITapGestureRecognizer {
        func didTapHighlightedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
            guard let attributedText = label.attributedText else {
                return false
            }
    
            let mutableStr = NSMutableAttributedString(attributedString: attributedText)
            mutableStr.addAttributes([NSAttributedString.Key.font: label.font!], range: NSRange(location: 0, length: attributedText.length))
               
            // If the label have text alignment. Delete this code if label have a default (left) aligment. Possible to add the attribute in previous adding.
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.alignment = label.textAlignment
            mutableStr.addAttributes([NSAttributedString.Key.paragraphStyle: paragraphStyle], range: NSRange(location: 0, length: attributedText.length))
    
            // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
            let layoutManager = NSLayoutManager()
            let textContainer = NSTextContainer(size: CGSize.zero)
            let textStorage = NSTextStorage(attributedString: mutableStr)
               
            // Configure layoutManager and textStorage
            layoutManager.addTextContainer(textContainer)
            textStorage.addLayoutManager(layoutManager)
               
            // Configure textContainer
            textContainer.lineFragmentPadding = 0.0
            textContainer.lineBreakMode = label.lineBreakMode
            textContainer.maximumNumberOfLines = label.numberOfLines
            let labelSize = label.bounds.size
            textContainer.size = labelSize
               
            // Find the tapped character location and compare it to the specified range
            let locationOfTouchInLabel = self.location(in: label)
            let textBoundingBox = layoutManager.usedRect(for: textContainer)
            let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                              y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
            let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x,
                                                         y: locationOfTouchInLabel.y - textContainerOffset.y)
            let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
            return NSLocationInRange(indexOfCharacter, targetRange)
        }
    }

    Trong hàm này mình sẽ dùng NSLayoutManager để xác định xem điểm chạm của người dùng mà UITapGestureRecognizer bắt được có đúng vị trí highlight text hay không.

    func này sẽ trả về cho chúng ta biết được vị trí người dùng đang chạm vào UILabel có trùng với range truyền vào hay không.

    Để ứng dụng nó vào bài toán của chúng ta thì chúng ta sẽ thực hiện nó như sau:

        private func setUpLabelInfo() {
            lbInfo.highlightText("More...", highlightColor: .red, in: "Two one-offs, a roadster and a coupé, mark the end of production of super sports cars powered by the V12 combustion engine in the lead-up to the hybrid era. More...")
            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapMore(_:)))
            lbInfo.isUserInteractionEnabled = true
            lbInfo.addGestureRecognizer(tapGesture)
        }
        
        @objc
        private func tapMore(_ gesture: UITapGestureRecognizer) {
            let mainText = lbInfo.text ?? ""
            let highlightText = "More..."
            let highlightTextRange = ((mainText) as NSString).range(of: highlightText)
            if gesture.didTapHighlightedTextInLabel(label: lbInfo, inRange: highlightTextRange) {
                print("Did tap: \(highlightText), hightlightRange: \(highlightTextRange)")
            }
        }

    Ở viewDidLoad chúng ta chỉ cần gọi nó ra là xong.

       override func viewDidLoad() {
            super.viewDidLoad()
    
            lbTitle.textColor = .orange
            lbTitle.textAlignment = .center
            lbTitle.text = "Lambo"
            setUpLabelInfo()
        }

    Kết quả thu được như sau:

    Vậy là chúng ta đã có thể thêm action vào cho text được highlight.

    Từ ví dụ này chúng ta có thể tuỳ biến cho phù hợp dự án của bạn hoặc phát triển nó lên theo cách mà các bạn mong muốn.

    Mình hi vọng bài viết giúp cho các bạn có thêm phương án lựa chọn khi tham gia những dự án có yêu cầu tương tự. Chúc các bạn thành công!

  • OWASP Top 10 for Mobile

    OWASP Top 10 for Mobile

    OWASP (Open Web Application Security Project) là dự án xây dựng một nền tảng hướng đến việc tăng cường bảo mật cho phần mềm. Bắt nguồn là cho các dự án Web app, Back end. Tuy nhiên hiện nay thì bảo mật cho các dự án Mobile cũng đã trở nên cấp thiết hơn rất nhiều, và series bài viết này sẽ hướng đến tìm hiểu về các vấn đề về security, phương thức tấn công, các phương pháp phòng tránh, các checklist để tạo ra một project mobile an toàn hơn.

    OWASP Top 10 for Mobile

    1. Improper Platform Usage

    Improper Platform Usage tức là sử dụng các chức năng (features) của nền tảng mobile (iOS/Android) một cách không phù hợp.

    Cụ thể, từng OS system sẽ cung cấp cho developers các tính năng (capabilities, features) có thể sử dụng để phát triển ứng dụng. Nếu developers không sử dụng các tính năng này để phát triển, hoặc sử dụng chúng một cách không chính xác, thì sẽ được gọi là improper use (sử dụng không đúng cách).

    Ví dụ: với iOS thì lưu các thông tin quan trọng của user như passworld hay token thì không được sủ dựng UserDefault để lưu mà cần lưu bằng Keychain, tuy nhiên, việc sử dụng sai config của Keychain cũng tiềm ẩn rủi ro bị tấn công lộ mật khẩu. Hoặc với những quyền truy cập vào thông tin user như Location, HealthKit, Photos cũng không được sử dụng một cách thiếu cân nhắc.

    2. Insecure Data Storage

    Insecure data storage, lưu trữ dữ liệu một cách không an toàn, không đơn thuần chỉ là cách lưu trữ data (ảnh, text, sql data…) một cách không an toàn. Mà còn có thể là log file, cookie file, cached file (URL, browser, third party data…). Một ví dụ điển hình là các developer rất hay sử dụng log trong quá trình phát triển phần mềm, rất dễ tiềm ẩn nguy cơ "lỡ tay" log ra các thông tin nhạy cảm mà quên không loại bỏ, dẫn đến việc bị lọt ra môi trường product.

    Những item cần cân nhắc khi nghĩ đến nguy cơ lộ data:

    • Cách OS cache data, cache image, có thể là cả logging, buffer, thậm chí key-press (đặc biệt là OS mở như Android)
    • Cách các framework được sử dụng trong dự án cache data
    • Cách các thư viện open source cache data, gửi / nhận data

    3. Insecure Communication

    Các ứng dụng mobile thì luôn cần phải transfer data, có thể là giữa client – server, giữa các devices với nhau. Và trong quá trình gửi nhận data, nếu không tuân thủ các quy tắc bảo mật thì có thể bị kẻ xấu tấn công ăn cắp các thông tin nhạy cảm. Đó có thể là password, thông tin account, hoặc các thông tin private của user.

    Việc tấn công ăn cắp thông tin có thể thông qua wifi mà devices đang truy cập, các router nhà mạng hoặc các thiết bị ngoại vi BLE, NFC…

    4. Insecure Authentication

    Insecure Authentication mô tả việc thực hiện xác thực user kém bảo mật dẫn đến người khác có thể lợi dụng để tấn công vào backend nhằm ăn cắp thông tin hoặc thực hiện các request tổn hại đến hệ thống. Vấn đề này có thể do việc thiết kế backend thiếu security như việc request không có access token, hoặc từ bản thân mobile app đã thực hiện việc authenticate offline sơ sài (ví dụ như set passcode ngắn để truy cập vào chức năng quan trọng, hoặc lưu password của user) dẫn đến việc kẻ tấn công có thể ăn cắp thông tin và truy cập vào server.

    5. Insufficient Cryptography

    Việc bảo mật dữ liệu trong mobile app thường áp dụng mã hóa. Tuy nhiên, trong một vài trường hợp thì mã hõa vẫn sẽ tiềm ân rủi ro bị tấn công ăn cắp dữ liệu. Một vài khả năng có thể kể đến như:

    • Quá ỷ lại vào mã hóa của OS (Built-In Code Encryption Processes), ví dụ iOS bản thân nó cũng sẽ mã hóa ứng dụng, tuy nhiên nếu devices bị jail break thì cũng có khả năng decode ứng dụng để lấy dữ liệu.
    • Sử dụng một cơ chế mã hóa đã lỗi thời hoặc tự viết lại thuật toán mã hóa.
    • Lưu trữ encrytion key một cách thiếu bảo mật.

    6. Insecure Authorization

    • Authentication: xác thực user
    • Authorization: xác thực quyền của user

    Việc thiếu sót trong việc xác thực quyền hạn của user trong hệ thống có thể dẫn đến sai sót trong việc cho phép user được truy cập vào những tài nguyên không được cho phép, hoặc có tính bảo mật cao. Dẫn đến việc mất mát các thông tin nhạy cảm của server hoặc bị thực thi những quyền có tính chất ảnh hưởng lớn đến hệ thống. Các vấn đề có thể xảy ra trong trường hợp này như:

    • Server không check quyền của user khi request lên, mà hoàn toàn dựa vào request của client, ví dụ người tấn công có thể lợi dụng bằng cách thêm các param vào GET/POST request để lừa server rằng "tao là admin" và có thể truy cập vào các thông tin nhạy cảm.
    • Một số trường hợp có thể server cung cấp các API dành riêng cho "admin" và nghĩ rằng các user bình thường sẽ không biết các API này nên không cần phải check quyền, tuy nhiên việc này không hề đảm bảo đến việc sẽ bảo mật được các API này mà không lộ ra ngoài cho các user không có quyền khác.

    7. Poor Code Quality

    Poor code quality là các lỗi bảo mật liên quan đến bản thân ngôn ngữ lập trình, có thể kể đến như lỗi:

    • Buffer overflows: các lỗi buffer overflows thì hay xảy ra với các thư viện viết bởi C, C++, và iOS cùng Android đề sử dụng các thư viện C, C++ trong hệ thống, do đó kẻ xấu có thể tấn công vào các thư viện này và qua đó thực thi các đoạn lệnh hoặc thậm chí cài mã độc vào ứng dụng.
    • Format string vulnerabilities: những lỗi dạng này thường là kẻ tấn công cố tình input các đoạn string mã hóa hoặc format đặc biệt vào ứng dụng, hoặc các câu lệnh để tấn công vào ứng dụng. Qua đó có thể gây ra các lỗi như crash ứng dụng, view data trong stack, view thông tin trong memory, thậm chí thực thi source code
    • Tấn công vào third party hoặc tấn công thông qua webview của OS: sử dụng các thư viện kém bảo mật cũng tiềm ẩn nguy cơ bị tấn công. Ngoài ra, trong các version OS cũ, cả iOS và Android đều bộc lộ rất nhiều lỗi liên quan đến webview như thực thi các đoạn javascript nhằm ăn cắp thông tin user.

    8: Code Tampering

    Các ứng dụng mobile, về cơ bản là sẽ nằm trên máy của user, do đó nên ứng dụng mobile rất dễ bị tấn công thay đổi nội dung source code để thực hiện các mục đích xấu. Có thể kể đến các kịch bản tấn công như: kẻ xấu sẽ cài đặt ứng dụng của bạn lên device đã bị jail, qua đó có thể xem được nội dung bên trong của ứng dụng như assets, resource. Thậm chí thay đổi source code hoặc các API được call. Mục đích của việc tấn công này có thể là unlock các chức năng ẩn, hoặc trả phí, hoặc ăn cắp thông tin. Một vài trường hợp có thể là cài mã độc vào, thay đổi asset, resource, sau đó lại distribute ứng dụng lên các kho ứng dụng lậu nhằm ăn cắp thông tin của người tải về. Trước đây thì mình hay jail device để cheat game, hoặc các ứng dụng ngân hàng cũng có thể là mục tiêu yêu thích để bị tấn công dạng này. -> Hãy check Jail/Root khi khởi động ứng dụng.

    9. Reverse Engineering

    Kẻ tấn công sẽ down ứng dụng và sử dụng các tool để tấn con giải mã source code nhằm các mục đích như ăn cắp thông tin trong string table/plist, đọc source code, tìm hiểu thuật toán hoặc các thư viện mà app sử dụng. Với kiểu tấn công này thì mục tiêu tấn công thường là

    • Ăn cắp thông tin về backend server, như url, path, cert nếu có
    • Ăn cắp các key của thuật toán mã hóa
    • Ăn cắp các thông tin liên quan đến sở hữu trí tuệ

    10. Extraneous Functionality

    Các chức năng không liên quan đến ứng dụng?

    Khi ứng dụng được phát triển, trên thực tế nó có rất nhiều các chức năng không thuộc functional requirement và non-functional requirement, nhưng là không thể thiếu trong quá trình phát triển ứng dụng như:

    • Log file, log console để debug những thông tin API, thông tin user hoặc thông tin của ứng dụng để debugging trong quá trình phát triển
    • Các switch flag để on off các chức năng nào đó trong ứng dụng, có thể là chức năng của admin hoặc chức năng liên quan đến A-B testing…
    • Các API path nhằm debug nhưng quên không loại bỏ khi lên môi trường production.

    Các chức năng này nếu bị khai thác có thể làm lộ thông tin về ứng dụng cũng như thông tin về user.

  • Hướng dẫn cách Gen Document Design bằng code Python

    Hướng dẫn cách Gen Document Design bằng code Python

    Hi các bạn, có bao giờ các bạn gặp phải tình huống khi kết thúc dự án thì phải làm Document design cho dự án của mình? Yêu cầu phải liệt kê hết các func trong project ra file excel và giải thích nó làm gì. Nếu câu trả lời là có thì các bạn có thể đọc tiếp bài viết này để xem cách thực hiện nó như thế nào nhé.

    Yêu cầu

    • Cần liệt kê hết tất cả các func trong souce code và giải thích func đó dùng để làm gì
    • Định dạng yêu cầu theo file excel

    Chuẩn bị

    Do tool được code trên nền tảng python nên mọi người cần cài đặt Python3 trước để có thể thực hiện gen document. Để cài đặt thì mọi người có thể làm theo hướng dẫn sau:

    1. Homebrew

    Link tham khảo: https://brew.sh v/bin/bash -c “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)”

    2. Install Python 3 on Mac – Brew Install

    Link tham khảo: https://www.freecodecamp.org/news/python-version-on-mac-update/ vbrew install pyenv // Install vpyenv install 3.9.2 // Update version

    3. Install pip: pip la một package của Python

    Link tham khảo: https://pip.pypa.io/en/stable/

    Download get-pip.py provided by https://pip.pypa.io hoặc sử dung terminal: vcurl https://bootstrap.pypa.io/get-pip.py -o get-pip.py vsudo python3 get-pip.py

    4. Openpyxl install: Cài đặt openpyxl sử dụng pip.

    Link tham khảo: https://openpyxl.readthedocs.io/en/stable/ vsudo pip install openpyxl

    5. Tải sẵn file template ở đây:

    File design là file excel template
    File DocC+.py là file code python nhằm mục đích gen gen souce và update file excel.

    NOTE: Đây là code dùng để gen dự án sử dụng ngôn ngữ lập trình swift, nếu các bạn cần gen trên ngôn ngữ khác chúng ta sẽ cần thực hiện chỉnh sửa file DocC+.py cho phù hợp với ngôn ngữ mà bạn sử dụng.

    Hướng dẫn sử dụng

    Bước 1: Copy file DocC+.py và design.xlsx vào trong thư mục cần gen.

    Bước 2: Bật Terminal -> navigate đến project folders chữa file DocC+.py

    Bước 3: Chạy câu lệnh dưới đây: python3 DocC+.py

    Vậy là chúng ta đã gen xong file liệt kê tất cả các func có trong thư mục mà các bạn chọn. Rất nhanh gọn và tiện lợi đúng không?

    Ưu điểm

    • Tiết kiệm rất nhiều thời gian thực hiện, thay vì phải copy bằng tay mất rất nhiều thời gian và khiến người thực hiện khá stress thì tool giúp chúng ta làm nó trong vài phút
    • Có thể sử dụng cho tất cả các ngôn ngữ lập trình
    • Quá dễ sử dụng

    Tổng kết

    Vậy là bài viết trên mình đã giới thiệu, chia sẻ và hướng dẫn các bạn sử dụng một tool cực kì hữu dụng và dễ sử dụng. Mình hi vọng nó sẽ giúp các bạn giải quyết bài toán mà bạn gặp phải. Chúc các bạn thành công!

  • Hướng dẫn cách sử dụng Lottie Animation trong Swift

    Hướng dẫn cách sử dụng Lottie Animation trong Swift

    Có lẽ tất cả mọi người trên thế giới đều yêu thích cái đẹp và mình cũng không ngoại lệ. Việc xây dựng những ứng dụng có UI đẹp mắt, animation mượt mà và thú vị khiến hàng triệu người dùng trầm trồ là mong muốn của rất nhiều những Developer trên toàn thế giới. Là một developer nếu bạn có tìm thấy bài viết này của mình thì chắc hẳn bạn cũng có khát khao làm những điều thú vị cho ứng dụng của mình có đúng không? Vậy chúng ta cùng đi tiếp vào bài viết này để làm cách nào để có thể làm ứng dụng của các bạn trở nên thú vị hơn nhé.

    Trong một dịp mình có cơ hội làm việc trong một dự án có sử dụng rất nhiều Animation để làm cho ứng dụng trở nên đẹp và thú vị hơn với người dùng. Lúc mới vào dự án khi sử dụng một số tính năng có sử dụng animation làm mình thấy rất hứng thú, thật khó để cưỡng lại sự đẹp đẽ và hoa mỹ của nó. Trong khi làm việc trong dự án thì mình cũng đã phát hiện ra một thư viện hỗ trợ cho việc thực hiện animation rất hay đó chính là Lottie. Vì vậy ngày hôm nay mình muốn chia sẻ với các bạn về thư viện này và cách thêm nó vào ứng dụng của các bạn.

    Lottie là gì?

    Lottie là định dạng tệp hoạt hình dựa trên JSON cho phép các nhà thiết kế gửi animation trên bất kỳ nền tảng nào dễ dàng như vận chuyển nội dung tĩnh. Bạn có thể đọc thêm về nó ở đây.

    Lottie được sử dụng khi nào?

    Lottie khá là linh hoạt và nó có thể sử dụng được trên nhiều nền tảng khác nhau từ iOS, Android, Web …. Vì vậy nó giúp đồng bộ animation trên tất cả các nền tảng mà không xảy ra sai sót nào.

    Lottie hỗ trợ làm animation rất tốt, nó có thể làm được những animation rất phức tạp mà việc code animation không thể làm được hoặc làm đơn giản hoá việc thêm animation vào trong ứng dụng.

    Hiện nay nó đang được sử dụng khá phổ biến trên các ứng dụng.

    Làm sao để sử dụng Lottie cho ứng dụng của bạn?

    Bước 1: Tạo dự án mới

    Nếu bạn muốn thêm vào dự án có sẵn của mình thì bỏ qua bước này nhé.

    Đầu tiên chúng ta mở Xcode lên và tạo một ứng dụng demo để có thể test Lottie chạy trên ứng dụng một cách dễ dàng hơn.

    Tạo ứng dụng mới

    Bước 2: Thêm thư viện Lottie vào ứng dụng

    Để thêm thư viện Lottie vào ứng dụng chúng ta có thể sử dụng bằng nhiều cách khác nhau như Cocoa Pods, Carthage hoặc Swift Package Manager. Nếu bạn chưa biết cách thêm thư viện Lottie vào dự án thì bạn có thể tham khảo hướng dẫn tại đây.

    Ở bài viết này mình sẽ hướng dẫn các bạn sử dụng Swift Package Manager để thêm vào ứng dụng như sau:

    Bạn mở dự án của bạn trên Xcode, chọn File -> Add Packages… Trên đầu bên phải của popup hiện ra có công cụ tìm kiếm các bạn đánh link: https://github.com/airbnb/lottie-ios.git để tìm thư viện. Sau đó bấm vào Add Package và chờ một lúc để Xcode tải thư viện bạn, sau khi tải xong bạn bấm Add để hoàn tất

    Bước 3: Thêm Lottie vào dự án

    Đầu tiên chúng ta cần chuẩn bị file Lottie JSON, nếu bạn chưa có thì bạn có thể download nó tại đây. Bạn nhớ tải file JSON nhé.

    Bạn cũng có thể sử dụng ứng dụng Adobe After Effect để tạo file Lottie của riêng mình.

    Khi đã có file lottie JSON rồi chúng ta sẽ thực hiện thêm file vào trong dự án bằng cách kéo thả nó trực tiếp vào thư mục trong Xcode của bạn, tại nơi mà bạn muốn lưu nó. Thông thường chúng ta tạo mới thư mục Resouces/LottieJSON và lưu nó trong đó.

    Add file lottie vào dự án

    Hãy nhớ bạn tích Copy Items if needed và các mục như trên hình nhé.

    Thêm Lottie bằng cách sử dụng code

    Đầu tiên để sử dụng được thư viện Lottie hãy nhớ import thư viện Lottie vào màn hình mà bạn sử dụng

    import Lottie

    Tiếp theo chúng ta tạo func addLottieAnimation() để thực hiện nhiệm vụ add lottie vào view hiện tại. Bạn cũng có thể custom một func riêng để handle xử lí triệt để các logic của Lottie animation. Trong bài này mình chỉ hướng dẫn cơ bản để các bạn có thể thêm Lottie vào ứng dụng của mình.

        func addLottieAnimation() {
            // 1. Create lottie animation view
            let lottieView: LottieAnimationView = LottieAnimationView(name: "404-Notfound")
            lottieView.frame = view.bounds
            
            // 2. Set animation content mode
            lottieView.contentMode = .scaleAspectFit
            
            // 3. Set animation loop mode
            lottieView.loopMode = .loop
            
            // 4. Adjust animation speed
            lottieView.animationSpeed = 0.5
            
            view.addSubview(lottieView)
            
            // 5. Play animation
            lottieView.play()
        }

    Sau đó bạn call func này ở viewDidLoad thì sẽ nhận được kết quả như sau:

    Thêm Lottie bằng cách sử dụng Builder Interface

    Đầu tiên bạn mở file giao diện của bạn lên băng Interface Builder, kéo một UIView và thực hiện constraint cho nó.

    Thêm UIview vào để sử dụng Lottie

    Sau đó ở trên cùng của tab bên phải bạn chọn Identity Inspector và thay đổi giá trị như hình dưới

    Thay đổi class sang LottieAnimationView

    Tiếp tục chuyển sang tab Attributes Inspector để điền tên file Lottie mà bạn muốn

    Tạo một liên kết giữa view của bạn với file controller để sử dụng, sau đó bạn thực hiện code như dưới đây để Lottie có thể hoạn động.

    import UIKit
    import Lottie
    
    class ViewController: UIViewController {
        @IBOutlet weak var animationLottieView: LottieAnimationView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            // 1. Set animation content mode
            animationLottieView.contentMode = .scaleAspectFit
    
            // 2. Set animation loop mode
    
            animationLottieView.loopMode = .loop
    
            // 3. Adjust animation speed
    
            animationLottieView.animationSpeed = 0.5
    
            // 4. Play animation
            animationLottieView.play()
        }
    }

    Chạy ứng dụng và bạn sẽ nhận được kết quả như mong đợi!

    Vậy là mình đã giới thiệu và hướng dẫn cho các bạn một thư viện khá hiệu quả cho các bạn sử dụng để bổ trợ cho các bạn việc thực hiện animation tốt hơn, dễ dàng hơn, và nhanh hơn so với cách code truyền thống. Từ giờ các bạn có thể tha hồ sáng tạo trên những ứng dụng sắp tới của bản thân và khiến mọi người sử dụng thích thú.

    Mình hi vọng nó sẽ giúp các bạn làm ra những ứng dụng có trải nghiệm tuyệt vời, giúp những người sử dụng ứng dụng của các bạn phải trầm trồ khi sử dụng nó. Chúc các bạn thành công với những dự án sắp tới của mình.

  • Architecture pattern: Clean Swift

    Architecture pattern: Clean Swift

    Xin chào mọi người! Lại là DaoNM2 đây! Tiếp tục với series về Architecture pattern hôm nay mình sẽ giới thiệu cho các bạn một kiến trúc khá mới so với các mẫu kiến trúc hiện tại đó là Clean Swift (VIP). Trước đây mình đã có cơ hội tiếp cận với kiến trúc này, khi tham gia vào việc xây dựng một ứng dụng rất lớn cho một công ti rất nổi tiếng về ô tô xe máy ở Việt Nam. Khi đó mình cũng đã tích luỹ được một số kinh nghiệm về kiến trúc này, vì vậy mình muốn chia sẻ với các bạn một số thông tin cũng như kinh nghiệm mà mình đã tích luỹ được khi làm việc với mẫu kiến trúc này.

    Đầu tiên nếu bạn là người mới hoặc bạn mới tìm hiều về các mẫu kiến trúc trong lập trình bao giờ đây là bài đầu tiên bạn đọc, thì để có thể hiểu kiến trúc này tốt hơn thì bạn có thể tham khảo các bài viết về các mẫu thiết kế trước khi tiếp tục ở link dưới đây:

    Bối cảnh hình thành

    Clean swift lần đầu tiên được giới thiệu bởi Raymond Law trên website clean-swift.com của anh ấy. Ý tưởng hình thành mẫu kiến trúc này là do anh ấy đã quá chán với các vấn đề của MVC(một mẫu kiến trúc mà Apple khuyên dùng) vì vậy anh ấy đã nghĩ ra Clean Swift để giải quyết các vấn đề mà các mẫu kiến trúc trước đây chưa làm được. Clean Swift được Raymond Law xây dựng dựa trên Clean Architecture của Uncle Bob.

    Clean Swift là gì?

    Clean Swift là một mẫu kiến trúc xây dựng dựa trên Clean Architecture của Uncle Bob để áp dụng cho việc xây dựng các ứng dụng iOS và MacOS.

    Trong Clean Swift Architecture pattern thì tất cả logic của ứng dụng sẽ được chia đều ra 3 thành phần chính của nó là View controller, Interactor và Presenter. Mỗi phần sẽ đảm nhiệm một số logic cụ thể, và chúng liên lạc với nhau bằng các liên kết 1 chiều, vì vậy source code của bạn sẽ luôn luôn đi theo một chiều chứ không đa chiều như các architecture pattern khác.

    Khi ứng dụng Clean Swift vào trong dự án của bạn, nó sẽ được cấu trúc theo từng màn hình của ứng dụng(scenes).

    Các thành phần của Clean Swift

    Mẫu kiến trúc Clean Swift được cấu tạo bởi các phần như sau:

    • View
    • View Controller
    • Router
    • Presenter
    • Interactor
    • Worker(optional)
    • Model(optional)
    Clean Swift architecture pattern

    Clean Swift gồm 3 phần chính là ViewController, Presenter và Interactor. 3 phần này có liên kết 1 chiều và tạo thành 1 vòng tròn. Khi View controller nhận được request nó sẽ gọi sang Interactor để nó xử lí logic, khi xong logic Interactor sẽ gửi dữ liệu sang bên Presenter để nó thực hiện format lại dữ liệu rồi trả về cho ViewController làm nhiệm vụ update lên View cho người dùng. Các thành phần này sẽ được kết nối với nhau bằng protocol.

    View

    Là các thành phần nằm trong UIKit hoặc bất kể thứ gì liên quan tới UI ví dụ như: storyboard, xib, UIView, UIControl …

    View Controller

    Định nghĩa các màn hình(scenes), nó có thể chứa 1 hoặc nhiều View

    Nó sẽ giữ các instances của Interactor và Router

    Là nơi nhận các tương tác của người dùng và gọi đến Interactor hoặc Router để xử lý, nó cũng nhận output của Presenter làm input và truyền nó lên view để hiển thị cho người dùng.

    Interactor

    Chứa các business logic của màn hình

    Giữ instance của Presenter và các Workers(nếu có)

    Nhận thông tin input từ ViewController và xử lý hoặc yêu cầu Worker làm việc để truyền kết quả sang cho Presenter

    Interactor sẽ không được import UIKit để đảm bảo source không có liên kết trực tiếp với View

    Presenter

    Giữ một tham chiếu yếu đến View Controller để truyền dữ liệu sang View Controller

    Là nơi xử lý logic hiển thị, khi nhận được input từ Interactor nó sẽ thực hiện format lại dữ liệu và truyền sang cho ViewController để nó hiển thị thông tin cho người dùng.

    Worker

    Là nơi được coi là trung tâm dữ liệu, nó sẽ thực hiện các nhiệm vụ liên quan tới việc lấy dữ liệu từ API hoặc LocalDB

    Là thành phần phụ nên ở các màn hình đơn giản không có tương tác với dữ liệu chúng ta có thể bỏ qua worker

    Router

    Nó giữ một tham chiếu yếu tới View Controller, nhằm mục đích tránh việc tham chiếu lẫn nhau(retain cycles) dẫn đến không thể release các đối tượng này khi nó không còn được sử dụng -> Lack memory.

    Router sinh ra để giảm tải công việc cho View Controller nó sẽ làm nhiệm vụ điều hướng trong ứng dụng.

    Model

    Nó là nơi định nghĩa các đối tượng cho ứng dụng, nó chỉ làm nhiệm vụ định nghĩa các đối tượng và không có xử lí logic hay liên kết trực tiếp với các thành phần khác của kiến trúc.

    Các đối tượng trong model sẽ được khai báo theo value type(struct, enum)

    Tương tự như Worker, một số màn hình đơn giản không tương tác với dữ liệu sẽ không cần đến Model

    Ưu điểm

    • Dễ maintain, fixbugs vì liên kết 1 chiều giữa các thành phần
    • Hỗ trợ viết Unit test một cách dễ dàng
    • Viết các phương thức ngắn hơn với trách nhiệm duy nhất
    • Tách được business logic sang cho Interactor xử lí
    • Có thể tái sử dụng các Workers và Services
    • Có thể áp dụng được cho các dự án lớn để giảm tình trạng conflict khi merge source.

    Nhược điểm

    • Quá nhiều các protocol với các nhiệm vụ riêng biệt, làm cho việc đặt tên trở nên khó khăn và không cẩn thận sẽ gây khó hiểu cho người đọc
    • Kích thước ứng dụng lớn do chứa nhiều protocol và nhiều file
    • Cần thời gian để cho các thành viên dự án có thể hiểu và tuân theo

    Tổng kết

    Clean swift là một mẫu kiến trúc không phổ biến như các mẫu khác, tuy nhiên ưu điểm của nó mang lại là rất lớn. Để vận hành các dự án lớn có yêu cầu viết Unitest chúng ta có thể coi Clean Swift là một trong những ứng cử viên sáng giá. Mình hi vọng bài viết có thể giúp các bạn có thêm kiến thức về mẫu kiến trúc Clean Swift và giúp các bạn có thể chọn được mẫu kiến trúc ưng ý cho những dự án sắp tới.

    Nếu các bạn muốn biết thêm nhiều thông tin hơn về Clean Swift Architecture Pattern thì các bạn có thể tham khảo tại link sau: Clean Swift

  • Architecture Pattern: VIPER trong iOS

    Architecture Pattern: VIPER trong iOS

    Xin chào các bạn, lại là DaoNM2 đây! Để tiếp tục series về Architecture patterns thì hôm nay mình xin giới thiệu cho các bạn một mẫu kiến trúc được sử dụng khá nhiều khi phát triển các ứng dụng di động đó là VIPER.

    VIPER là gì?

    VIPER là một mẫu kiến trúc để phát triền phần mềm, nó được sử dụng khá nhiều khi xây dựng các ứng dụng di động trên ngôn ngữ lập trình Swift. Nó được xây dựng dựa trên Clean Design Architecture. Các Modules trong VIPER được định hướng theo Protocol và mỗi chức năng, các thuộc tính input và output được thực hiện bằng các bộ quy tắc giao tiếp cụ thể.

    Các thành phần chính của VIPER architecture pattern

    VIPER là viết tắt của các chứ cái đầu trong các thành phần của nó, nó bao gồm View, Interactor, Presenter, Entity và Router. Các thành phần này sẽ tương tác với nhau như sơ đồ dưới đây:

    View

    Bao gồm các thành phần trong UIKit và ViewController, nó là nơi để hiển thị nội dung cho người dùng và nhận các tương tác từ người dùng sau đó gửi cho presenter để xử lí tiếp logic hiển thị. Trong mẫu kiến trúc này Presenter là tầng duy nhất có liên kết với View.

    Interactor

    Là nơi xử lý business logic của ứng dụng, nó sẽ thao tác với Entity, model, API fetcher và datastore. Khi nhận được request từ Presenter lúc này Interactor sẽ thực hiện logic để lấy dữ liệu tương ứng và trả về cho presenter.

    Trong VIPER mỗi một Interactor sẽ tương ứng với một Use case, nó tách biệt hoàn toàn với View vì vậy khả năng kiểm thử độc lập trên Interactor khá dễ dàng.

    Presenter

    Là nơi xử lý logic hiển thị của ứng dụng, khi nhận được request thay đổi hoặc hiển thị thông tin từ View nó sẽ thực hiện logic tương ứng để yêu cầu Interactor trả về data. Sau khi nhận được data nó sẽ format lại dữ liệu và trả về cho View để hiển thị chúng lên màn hình. Khi nhận được yêu cầu di chuyển màn hình Presenter sẽ thực hiện call Router để nó làm nốt nhiệm vụ điều hướng

    Entity

    Đây là các Data model, nó có nhiệm vụ tương tác với Interactor để trả dữ liệu về cho Presenter.

    Router

    Là nơi xử lí luồng của ứng dụng, nó làm nhiệm vụ điều hướng ứng dụng đến nơi mà người dùng cần. Khi Presenter nhận yêu cầu chuyển màn hình từ View, nó sẽ thực hiện logic hiển thị và thực hiện tương tác với Router để xử lí di chuyển luồng đúng với yêu cầu của View.

    Ưu điểm

    VIPER được chia nhỏ thành nhiều phần, các phần đảm nhiệm các vai trò và nhiệm vụ cố định, các thành phần tương tác với nhau dựa trên các quy định cụ thể vì vậy nó có khá nhiều ưu điểm

    • Các nhiệm vụ được chia đều ra cho các thành phần vì vậy việc maintain không còn quá rắc rối.
    • Việc kiểm thử (Unit test) cũng trở nên dễ dàng hơn vì giờ đây các thành phần đã được chia nhỏ và không liên kết chặt chẽ với View
    • Cấu trúc source trở nên dễ hiểu và rõ ràng hơn vì nó được chia theo từng use case và các phần được chia nhiệm vụ và trách nhiệm rõ ràng
    • Không gặp phải trường hợp một file có nội dung quá dài, vì vậy việc đọc source code của người cũng trở nên dễ hiểu hơn
    • Khá là hữu dụng với các ứng dụng lớn với team size lớn
    • Dễ dàng để mở rộng và bảo trì, các developer có thể đồng thời làm việc trên nó một cách trơn tru
    • Giảm số lượng conflict khi merge source code

    Nhược điểm

    • Do có nhiều thành phần và tương tác với nhau nên số file quản lý sẽ nhiều hơn so với các mẫu kiến trúc khác
    • Không dễ sử dụng cho người mới vì có nhiều ràng buộc và quy tắc cho từng phần, vì vậy cần thời gian để các member có thể tìm hiểu và thích nghi với mẫu kiến trúc này.
    • Một số thư viện bên thứ 3 không hỗ trợ kiến trúc này, vì vậy nếu không có lựa chọn nào khác lúc này nếu áp dụng thư viện vào ứng dụng nó sẽ phá vỡ kiến trúc ở các tính năng mà sử dụng thư viện này.

    Tổng kết

    Như mình đã phân tích ở trên VIPER có rất nhiều ưu điểm vì vậy nó rất đáng để các bạn tìm hiểu và sử dụng cho các dự án sắp tới. Tuy nhiên theo mình thì mẫu kiến trúc VIPER chỉ nên sử dụng cho những ứng dụng có kích thước vừa và lớn thì nó mới phát huy được tối đa sự hiệu quả. Đối với các dự án nhỏ nếu sử dụng VIPER architecture pattern thì nó lại trở nên quá cồng kềnh và không cần thiết.

    Mình hi vọng bài viết mình chia sẻ sẽ giúp các bạn có thêm lựa chọn khi bắt đầu một dự án mới!

    Chúc các bạn thành công!

  • Architecture Pattern: MVVM trong iOS

    Architecture Pattern: MVVM trong iOS

    Chào các bạn, để tiếp tục series về Architecture pattern thì hôm nay mìn sẽ giới thiệu đến một mô hình có thể giải quyết được một số nhược điểm của các mô hình cũ như MVC, MVP.

    Nếu các bạn chưa tiếp cận hoặc chưa tìm hiều về các Architecture Pattern bao giờ thì có thể xem lại các bài viết của mình về MVC hoặc MVP tại đây:
    iOS Architecture Patterns: Cocoa MVC
    MVP Architecture Pattern và biến thể MVP-C
    Để khi đi vào bài viết này chúng ta sẽ dễ dàng hiểu được nội dung bài viết truyền tải.

    Lịch sử hình thành và phát triển

    MVVM được viết đầy đủ là Model View ViewModel, MVVM là một biến thể của mẫu thiết kế Presentation Model của Martin Fowler. Nó được sáng lập ra bởi các kiến trúc sư của Microsoft tên là Ken Cooper và Ted Peters, nó đặc biệt được sinh ra để làm đơn giản việc lập trình hướng sự kiện (event-driven programming). MVVM được tích hợp vào Windows Presentation Foundation (WPF) (hệ thống đồ họa .NET của Microsoft) và Silverlight, dẫn xuất ứng dụng Internet của WPF. John Gossman, một kiến ​​trúc sư Microsoft WPF và Silverlight, đã công bố MVVM trên blog của mình vào năm 2005.

    MVVC là gì?

    MVVC là một mẫu kiến trúc giúp tách biệt source code của bạn ra thành nhiều thành phần khác nhau. Nó giúp code của bạn có các thành phần độc lập, giúp cho quá trình phát triển và kiểm thử ứng dụng trở nên rõ dàng và dễ dàng hơn.

    Cấu tạo của MVVM

    MVVM gồm 3 phần chính là Model, View và ViewModel.

    MVVM

    Model

    Là nơi chứa dữ liệu và xử lí business logic, model sẽ thực hiện các công việc như lưu trữ các data được lấy về từ API, local storage, v.v. Nó độc lập so với View và tương tác với View thông qua ViewModel.

    View

    Là nơi hiển thị giao diện cho người dùng, nhận các sự kiện từ người dùng, xử lí và gửi các yêu cầu của người dùng cho ViewModel xử lí. View trong iOS thì thông thường là các thành phần của UIKit, storyboard, xib …, ở MVVM trong iOS thì View bao gồm cả các View Controller, nó sẽ là thành phần cài đặt cho View và gửi và nhận thông tin từ ViewModel.

    ViewModel

    Là nơi xử lí các logic hiển thị(presentation logic), nó là cầu nối giữa View và Model. ViewModel sẽ nhận yêu cầu từ View và lấy dữ liệu từ model về xử lí sau đó trả lại cho View thứ mà nó cần để hiển thị lên màn hình cho người dùng.

    Trong khi MVC thì Controller, MVP thì có Presenter làm trung gian giữa View và Model. Ở MVVM thì ViewModel cũng tương tự, nó là thành phần trung gian giúp kết nối View với Model.

    Ưu điểm của MVVM

    • Vì MVVM là mô hình nâng cấp của MVC, cho nên nó giúp app vẫn duy trì cấu trúc của mô hình MVC và bao gồm các ưu điểm của MVC
    • Giảm tải lượng code chứa trong View và View Controller.
    • Khi đó View và View Controller trở nên đơn giản hơn khi những logic.
      Ví dụ như logic về quy định cách hiển thị của dữ liệu, được chuyển hết sang ViewModel. Điều này khiến cho code trở nên dễ hiểu và dễ maintain hơn.
    • Sự liên lạc giữa các thành phần trong mô hình rõ ràng, khiến nó hoạt động tốt hơn với cơ chế binding dữ liệu.
    • Có thể thực hiện UnitTest lên tầng ViewModel.
    • Nhiệm vụ được chia đều cho các tầng

    Nhược điểm của MVVM

    • Nhiều file nên source code lại nhiều thêm
    • Tương tác giữa các thành phần phức tạp hơn các mẫu kiến trúc khác như MVC, MVP vì vậy người mới khó tiếp cận và thực hiện hơn.
    • Rắc rối trong việc phản hồi lại yêu cầu hơn so với các mẫu kiến trúc khác
    • Đối với nhưng dự án nhỏ thì nó lại quá cồng kềnh để thực hiện

    Tổng kết

    MVVM là một mẫu kiến trúc rất tốt khi bạn triển khai những ứng dụng có kích thước vừa và lớn, nó hỗ trợ UnitTest khá hiệu quả. Trong MVVM thì nhiệm vụ được chia đều cho các tầng vì vậy sẽ không quá khó để quản lý source code. Mình hi vọng bài viết giúp các bạn có thể dễ dàng hơn khi chon mẫu kiến trúc cho các dự án mới. Chúc các bạn thành công!

  • AVFoundation trên Swift và ứng dụng để xây dựng tính năng QR Scan

    AVFoundation trên Swift và ứng dụng để xây dựng tính năng QR Scan

    Xin chào tất cả các bạn

    Thời gian gần đây, như các bạn có thể thấy cứ bước chân ra khỏi nhà là thấy đâu đâu cũng có những ô mã QR. Từ việc đi đá bát phở cũng quét thanh toán QR, đăng nhập zalo hay telegram cũng có thể quét QR, hay thậm chí trà đá vỉa hè cũng có QR luôn…. Điều đó chứng tỏ mã QR đang dần được ứng dụng rất rộng rãi vào tất cả các lĩnh vực trong cuộc sống, thay thế cho giấy tờ truyền thống cũng như giúp cuộc sống trở nên tiện lợi hơn.

    Tuy nhiên nếu chỉ có mỗi mã QR thì cũng không giúp ích được gì khi mà không có những thiết bị đọc và giải mã những chiếc QR vi diệu này. Và những thiết bị để đọc mã QR cũng chẳng đâu xa ngay chính trên chiếc smart phone mà lúc nào cũng theo các bạn 24/7. Việc tích hợp QR Scan vào các ứng dụng di động ngày nay như một tính năng không thể thiếu cũng như giúp ứng dụng trở nên đa nhiệm hơn

    Thời gian vừa qua mình cũng có cơ duyên được trải qua một dự án về ngân hàng và phát triển tính năng QRPay, một tính năng mà như các bạn có thể thấy lúc nào cũng xuất hiện trên các ứng dụng Internet Banking cũng như các ví điện tử. Vậy nên ở bài viết này, mình xin chia sẻ các bạn cách tạo một ứng dụng Scan QR đơn giản và những framework liên quan trên iOS bằng ngôn ngữ lập trình Swift. Let’s go !

    Phần 1: Tìm hiểu về mã QR

    QR là viết tắt của Quick response có thể tạm dịch là mã phản hồi nhanh. Đây là dạng mã vạch có thể đọc được bởi một máy đọc chuyên dụng hoặc bằng smartphone có chức năng chụp ảnh kèm với ứng dụng cho phép quét mã. Mã QR còn có thể được gọi là Mã vạch ma trận (Matrix-barcode) hoặc Mã vạch 2 chiều (2D), là một dạng thông tin đã được mã hóa và có thể hiển thị để máy quét mã có thể đọc được.

    Mã QR là một mã vạch ma trận được phát triển bởi công ty Denso Wave vào năm 1994. Denso Wave là công ty con của Toyota. Mã QR gồm những chấm đen và các ô vuông trên nền trắng, nó thể chứa đa dạng các thông tin như URL, thông tin cá nhân, thời gian, địa điểm của một sự kiện nào đó, mô tả, giới thiệu một sản phẩm nào đó,…

    Phần 2: AV Foundation Framework trong Swift

    Như các bạn có thể thấy, để làm việc với Media trên các thiết bị iOS, Apple đã cung cấp rất nhiều các framework mạnh mẽ. Ở tầng cao nhất (high-level) là UIKit và AVKit, tầng thấp nhất (low-level) là CoreAudio, Core Media, Core Animation và ở giữa chính là nhân vật chính của chúng ta – AVFoundation

    UIKit framework giúp dễ dàng kết hợp tính năng chụp ảnh tĩnh và quay video cơ bản vào ứng dụng. Cả Mac OS X và iOS đều có thể sử dụng thẻ HTML5 <audio> và <video> bên trong WebView hoặc UIWebView để phát nội dung âm thanh và video. 

    Ngoài ra còn có AVKit framework, giúp đơn giản hóa việc xây dựng các ứng dụng phát video hiện đại. Tất cả các framework này đều thuận tiện và dễ sử dụng và nên thường được dùng khi thêm chức năng phương tiện vào ứng dụng. Tuy nhiên, mặc dù các framework này rất tiện lợi nhưng chúng thường thiếu tính linh hoạt và khả năng kiểm soát cần thiết cho các ứng dụng nâng cao hơn.

    Ở tầng thấp nhất, chúng ta có các low-level framework, cung cấp chức năng hỗ trợ và được sử dụng bởi tất cả các framework cấp cao hơn. Hầu hết chúng đều là các framework cấp độ thấp, cực kỳ mạnh mẽ và hiệu quả, nhưng rất phức tạp để tìm hiểu và sử dụng, đồng thời yêu cầu chúng ta phải hiểu rõ về cách phương tiện được xử lý ở cấp độ phần cứng.

    Chính vì vậy, vị cứu tính của chúng ta – AV Foundation nằm ở giữa low-level và high-level framework. Mang trong mình sức mạnh cũng như hiệu năng của các low-level framework nhưng lại dễ tiếp cận, dễ đọc hơn cho các developer. Nó có thể làm việc trực tiếp với các framework cấp thấp như Core Media và Core Audio và hoạt động với các high-level framework như Media Player và Assets Library. Như vậy chúng ta có thể đủ thấy sức mạnh của AVF mạnh như nào phải k ạ ^^

    AV Foundation là framework đầy đủ tính năng để làm việc với phương tiện nghe nhìn trên iOS, macOS, watchOS và tvOS. Sử dụng AV Foundation, chúng ta có thể dễ dàng phát, tạo và chỉnh sửa phim QuickTime và các tệp MPEG-4, HLS streams và xây dựng chức năng truyền thông mạnh mẽ vào ứng dụng.

    Với AV Foundation, chúng ta có thể tạo ra các ứng dụng liên quan đến chụp ảnh và quay video, phát nhạc,… nói chúng tất cả những thứ liên quan đến cụm từ Media trên mobile và ngoài ra nó còn kèm theo rất nhiều thứ hay ho như điều khiển đèn flash phía trước và phía sau, âm thanh cho video…vv.

    Như vậy có thể thấy AV Foundation framework khá lớn nên trong bài viết này, mình chỉ tìm hiểu một ứng dụng nhỏ của framework này đó là sử dụng camera trên thiết bị của Apple để đọc mã QR Code. Và chi tiết cách xây dựng ở phần 3 dưới đây.

    Phần 3: Xây dựng ứng dụng QR Scan đơn giản bằng AV Foundation framework trên Swift

    Đầu tiên chúng ta sẽ tạo một class QRScanCustomView như sau:

    import Foundation
    import UIKit
    import AVFoundation
    
    protocol QRScanCustomViewDelegate: AnyObject {
        func onDecodeFailed()
        func onDecodeSuccess(_ decodeString: String)
        func onCameraAccessDenied()
    }
    
    public class QRScanCustomView: UIView {
        weak var delegate: QRScanCustomViewDelegate?
        private var captureSession: AVCaptureSession?
        private var scanAreaWidth: CGFloat = 300.0
        private var scanAreaHeight: CGFloat = 300.0
        private var scanAreaXpos: CGFloat = 0.0
        private var scanAreaYpos: CGFloat = 0.0
        private var scanAreaCornerRadius: CGFloat = 8.0
        private var scanViewBackgroundColor = UIColor.black.withAlphaComponent(0.9)
        private var isAnimationFromTop: Bool = true
        private var animationTimer = Timer()
        private var animationView = UIView()
        private let gradientBackgroundView = UIView()
        private var gradientView = UIView()
        private let animationContainerView = UIView()
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        public override var layer: AVCaptureVideoPreviewLayer {
            if let layer = super.layer as? AVCaptureVideoPreviewLayer {
                return layer
            }
            return AVCaptureVideoPreviewLayer()
        }
    }
    
    extension QRScanCustomView {
        func startScanning() {
            captureSession?.startRunning()
        }
        
        func stopScanning() {
            captureSession?.stopRunning()
        }
        
        func startAnimation() {
            runAnimation()
            animationTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(runAnimation), userInfo: nil, repeats: true)
        }
        
        func stopAnimation() {
            animationTimer.invalidate()
        }
        
        func setSizeOfScanArea(width: CGFloat, height: CGFloat) {
            scanAreaWidth = width
            scanAreaHeight = height
        }
        
        private func commonInit() {
            self.clipsToBounds = true
            captureSession = AVCaptureSession()
            
            guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
                return
            }
            let videoInput: AVCaptureDeviceInput
            do {
                videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            } catch {
                delegate?.onCameraAccessDenied()
                return
            }
            
            if captureSession?.canAddInput(videoInput) ?? false {
                captureSession?.addInput(videoInput)
            } else {
                scanningDidFail()
                return
            }
            
            let metadataOutput = AVCaptureMetadataOutput()
            
            if captureSession?.canAddOutput(metadataOutput) ?? false {
                captureSession?.addOutput(metadataOutput)
                
                metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
                metadataOutput.metadataObjectTypes = [.qr]
            } else {
                scanningDidFail()
                return
            }
            
            layer.session = captureSession
            layer.videoGravity = .resizeAspectFill
            layer.frame = UIScreen.main.bounds
            
            captureSession?.startRunning()
            
            // MARK: SET AVAILABLE RANGE TO SCAN
            let rectView = layer.metadataOutputRectConverted(fromLayerRect: rectForScannerRange())
            metadataOutput.rectOfInterest = rectView
        }
        
        // MARK: Get rect of scan range
        private func rectForScannerRange() -> CGRect {
            scanAreaXpos = (UIScreen.main.bounds.size.width - scanAreaWidth) / 2
            scanAreaYpos = (UIScreen.main.bounds.size.height - scanAreaHeight) / 2
            let newRect = CGRect(x: scanAreaXpos, y: scanAreaYpos, width: scanAreaWidth, height: scanAreaHeight)
            return newRect
        }
        // MARK: Add view with clear scanner range
        func addQRScannerOverlayView(view: UIView) {
            view.addSubview(overlayView())
            view.layer.addSublayer(createCornerFrame(with: rectForScannerRange()))
            // Set authorize here to run it after qrScannerDelegate was declared
            setupScanAnimationView(view: view)
        }
        // MARK: Create scan range
        private func overlayView() -> UIView {
            let overlayView = UIView(frame: frame)
            overlayView.backgroundColor = scanViewBackgroundColor
            let path = CGMutablePath()
            path.addRoundedRect(in: rectForScannerRange(), cornerWidth: scanAreaCornerRadius, cornerHeight: scanAreaCornerRadius)
            path.closeSubpath()
            path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
            
            let maskLayer = CAShapeLayer()
            maskLayer.backgroundColor = UIColor.black.cgColor
            maskLayer.path = path
            maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
            overlayView.layer.mask = maskLayer
            overlayView.clipsToBounds = true
            
            return overlayView
        }
        // MARK: Draw focus corner frame
        private func createCornerFrame(with rect: CGRect) -> CAShapeLayer {
            let length: CGFloat = 24.0
            let radius: CGFloat = 8.0
            let offset: CGFloat = 8.0
            let lineWidth: CGFloat = 4.0
            let distance: CGFloat = rect.size.width - offset * 2 - length * 2 - radius * 2
            
            let path = CGMutablePath()
            
            var start = CGPoint(x: rect.origin.x + offset, y: rect.origin.y + offset + radius + length)
            var end = CGPoint(x: start.x + radius + length, y: start.y - length - radius)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x, y: start.y - length))
            path.addArc(tangent1End: CGPoint(x: start.x, y: start.y - length - radius),
                        tangent2End: CGPoint(x: start.x + radius, y: start.y - length - radius),
                        radius: radius)
            path.addLine(to: end)
            
            start = CGPoint(x: end.x + distance, y: end.y)
            end = CGPoint(x: start.x + length + radius, y: start.y + radius + length)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x + length, y: start.y))
            path.addArc(tangent1End: CGPoint(x: start.x + length + radius, y: start.y),
                        tangent2End: CGPoint(x: start.x + length + radius, y: start.y + radius),
                        radius: radius)
            path.addLine(to: end)
            
            start = CGPoint(x: end.x, y: end.y + distance)
            end = CGPoint(x: start.x - radius - length, y: start.y + length + radius)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x, y: start.y + length))
            path.addArc(tangent1End: CGPoint(x: start.x, y: start.y + length + radius),
                        tangent2End: CGPoint(x: start.x - radius, y: start.y + length + radius),
                        radius: radius)
            path.addLine(to: end)
            
            start = CGPoint(x: end.x - distance, y: end.y)
            end = CGPoint(x: start.x - length - radius, y: start.y - radius - length)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x - length, y: start.y))
            path.addArc(tangent1End: CGPoint(x: start.x - length - radius, y: start.y),
                        tangent2End: CGPoint(x: start.x - length - radius, y: start.y - radius),
                        radius: radius)
            path.addLine(to: end)
            
            let shape = CAShapeLayer()
            shape.path = path
            shape.strokeColor = UIColor.orange.cgColor
            shape.lineWidth = lineWidth
            shape.fillColor = UIColor.clear.cgColor
            shape.lineCap = .round
            return shape
        }
        // MARK: Create scan animation view, size of inputView = size of QRScanView
        private func setupScanAnimationView(view: UIView) {
            animationContainerView.backgroundColor = .clear
            animationContainerView.frame = rectForScannerRange()
            view.addSubview(animationContainerView)
            
            animationView.frame = CGRect(x: animationContainerView.bounds.minX - 8,
                                         y: animationContainerView.bounds.minY,
                                         width: animationContainerView.bounds.width + 16,
                                         height: 3.0)
            animationView.layer.cornerRadius = animationView.frame.height / 2
            animationView.backgroundColor = UIColor.orange
            animationContainerView.addSubview(animationView)
            animationContainerView.bringSubviewToFront(animationView)
            
            gradientBackgroundView.frame = animationContainerView.bounds
            gradientBackgroundView.backgroundColor = .clear
            animationContainerView.addSubview(gradientBackgroundView)
            animationContainerView.bringSubviewToFront(gradientBackgroundView)
            gradientBackgroundView.clipsToBounds = true
        }
        
        @objc
        private func runAnimation() {
            animationView.clipsToBounds = false
            if isAnimationFromTop {
                for subView in gradientBackgroundView.subviews {
                    subView.removeFromSuperview()
                }
                gradientBackgroundView.addSubview(createGradientViewEffect(rect: CGRect(x: 0,
                                                                                        y: -20,
                                                                                        width: gradientBackgroundView.bounds.width,
                                                                                        height: 20),
                                                                           isRevertColor: false))
                UIView.animate(withDuration: 2.0, animations: {
                    self.animationView.transform = CGAffineTransform(translationX: 0,
                                                                     y: self.animationContainerView.frame.size.height)
                    self.gradientView.transform = CGAffineTransform(translationX: 0,
                                                                    y: self.animationContainerView.frame.size.height)
                    Timer.scheduledTimer(timeInterval: 1.6, target: self, selector: #selector(self.hideGradientView), userInfo: nil, repeats: false)
                })
                
            } else {
                for subView in gradientBackgroundView.subviews {
                    subView.removeFromSuperview()
                }
                gradientBackgroundView.addSubview(createGradientViewEffect(rect: CGRect(x: 0,
                                                                                        y: gradientBackgroundView.bounds.height,
                                                                                        width: gradientBackgroundView.bounds.width,
                                                                                        height: 20),
                                                                           isRevertColor: true))
                UIView.animate(withDuration: 2.0, animations: {
                    self.animationView.transform = CGAffineTransform(translationX: 0,
                                                                     y: 0)
                    self.gradientView.transform = CGAffineTransform(translationX: 0,
                                                                    y: 0)
                    Timer.scheduledTimer(timeInterval: 1.6, target: self, selector: #selector(self.hideGradientView), userInfo: nil, repeats: false)
                })
            }
            isAnimationFromTop = !isAnimationFromTop
        }
        
        @objc
        private func hideGradientView() {
            UIView.animate(withDuration: 0.4, animations: {
                self.gradientView.alpha = 0
            })
        }
        
        private func createGradientViewEffect(rect: CGRect, isRevertColor: Bool) -> UIView {
            gradientView.layer.sublayers?.removeAll()
            gradientView.frame = rect
            let gradientLayer: CAGradientLayer = CAGradientLayer()
            gradientLayer.frame = gradientView.bounds
            let firstColor = UIColor.orange.withAlphaComponent(0.01).cgColor
            let secondColor = UIColor.orange.withAlphaComponent(1).cgColor
            gradientLayer.colors = isRevertColor ? [secondColor, firstColor] : [firstColor, secondColor]
            gradientLayer.locations = [0.0, 1.0]
            gradientView.layer.insertSublayer(gradientLayer, at: 0)
            gradientView.alpha = 0.6
            return gradientView
        }
        
        func scanningDidFail() {
            delegate?.onDecodeFailed()
            captureSession = nil
        }
        
        func scanSuccess(code: String) {
            delegate?.onDecodeSuccess(code)
        }
    }
    
    extension QRScanCustomView: AVCaptureMetadataOutputObjectsDelegate {
        public func metadataOutput(_ output: AVCaptureMetadataOutput,
                                   didOutput metadataObjects: [AVMetadataObject],
                                   from connection: AVCaptureConnection) {
            stopScanning()
            
            if let metadataObject = metadataObjects.first {
                guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else {
                    return
                }
                guard let stringValue = readableObject.stringValue else {
                    return
                }
                AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                scanSuccess(code: stringValue)
            }
        }
    }

    Để có thể sử dụng thư viện, chúng ta phải import thư viện “import AVFoundation” và khởi tạo “captureSession” để quản lý session capture. Có thể hiểu đơn giản rằng captureSession giúp quản lý việc sử dụng camera của chúng ta.

    Ngoài ra, vì ứng dụng của chúng ta sử dụng camera để capture mã QR nên phải khai báo Camera Usage Description trong Info.plist. Nếu không khai báo ứng dụng của chúng ta sẽ không thể chạy vì vi phạm policy của Apple

    Tiếp theo chúng ta sẽ khởi tạo AVCaptureVideoPreviewLayer:

    public override var layer: AVCaptureVideoPreviewLayer {
            if let layer = super.layer as? AVCaptureVideoPreviewLayer {
                return layer
            }
            return AVCaptureVideoPreviewLayer()
        }

    Layer này chính là một layer hiển thị lên màn hình có vai trò thực hiện việc sử dụng camera để scan mã QR

    Tạo ra các func để quản lý start, stop capture session:

       func startScanning() {
            captureSession?.startRunning()
        }
        
        func stopScanning() {
            captureSession?.stopRunning()
        }

    Ở func commonInit(), chúng ta sẽ tạo khởi tạo videoCaptureDevice để sử dụng kiểu capture video sử dụng camera của chúng ta và add vào kiểu input của captureSession, ở đây có thể hiểu là chúng ta đang khởi tạo đầu vào cho ứng dụng của chúng ta sử dụng camera để scan QR

            guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
                return
            }
            let videoInput: AVCaptureDeviceInput
            do {
                videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            } catch {
                delegate?.onCameraAccessDenied()
                return
            }
            
            if captureSession?.canAddInput(videoInput) ?? false {
                captureSession?.addInput(videoInput)
            } else {
                scanningDidFail()
                return
            }

    Sau khi thực hiện khởi tạo đầu vào, chúng ta sẽ khởi tạo đầu ra cho captureSession qua class AVCaptureMetadataOutput, ở đây mình đã custom một khoảng trắng giữa màn hình để tạo vùng scan bằng thuộc tính “rectOfInterest”. Nếu không gắn thuộc tính này, mặc định cả màn hình capture của chúng ra sẽ là vùng scan:

     let metadataOutput = AVCaptureMetadataOutput()
            
            if captureSession?.canAddOutput(metadataOutput) ?? false {
                captureSession?.addOutput(metadataOutput)
                
                metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
                metadataOutput.metadataObjectTypes = [.qr]
            } else {
                scanningDidFail()
                return
            }
            
            layer.session = captureSession
            layer.videoGravity = .resizeAspectFill
            layer.frame = UIScreen.main.bounds
            
            captureSession?.startRunning()
            
            // MARK: Set scan Area
            let rectView = layer.metadataOutputRectConverted(fromLayerRect: rectForScannerRange())
            metadataOutput.rectOfInterest = rectView

    Để hứng được output của captureSession. Chúng ta sẽ kế thừa delegate của AVCaptureMetadataOutput. Delegate này sẽ giúp chúng ta get được String decode được từ mã QR:

    extension QRScanCustomView: AVCaptureMetadataOutputObjectsDelegate {
        public func metadataOutput(_ output: AVCaptureMetadataOutput,
                                   didOutput metadataObjects: [AVMetadataObject],
                                   from connection: AVCaptureConnection) {
            stopScanning()
            
            if let metadataObject = metadataObjects.first {
                guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else {
                    return
                }
                guard let stringValue = readableObject.stringValue else {
                    return
                }
                AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                scanSuccess(code: stringValue)
            }
        }
    }

    Ngoài ra mình còn tạo ra các func để custom màn hình và tạo animation quét QR để trông ứng dụng của chúng ta trông đẹp mắt hơn. Các bạn có thể tham khảo ở phần source đầy đủ bên trên.

    Về cơ bản chỉ cần khởi tạo đủ AVCaptureSession, tạo ra input và output cho AVCaptureSession và kế thừa delegate để hứng output là chúng ta có thể tạo ra một ứng dụng scan QR đơn giản.

    Bài viết của mình vẫn đang trong quá trình update để đầy đủ và chi tiết hơn, nếu có góp ý gì mọi người có thể comment bên dưới để mình bổ sung và cải thiện thêm nhé.

    Cảm ơn mọi người đã quan tâm đến bài viết của mình. Chúc mọi người có một ngày làm việc hiệu quả !

  • MVP Architecture Pattern và biến thể MVP-C

    MVP Architecture Pattern và biến thể MVP-C

    Là một Developer, chắc hẳn các bạn đã trải qua nhiều dự án khác nhau. Thông thường khi bạn càng làm nhiều dự án bạn càng có nhiều cơ hội tiếp cận đến các loại Architecture pattern khác nhau như MVC, MVP, MVVM, VIPPER, … 

    Sau khi chinh chiến ở các dự án lớn nhỏ khác nhau mình cũng tích luỹ được một chút kiến thức về MVP Architecture pattern, vì vậy mình muốn viết một bài để chia sẻ một số kiến thức nho nhỏ mà mình đã học được về MVP cho những bạn chưa có cơ hội làm việc với MVP Architecture pattern.

    Lịch sử hình thành và phát triển

    MVP là viết tắt của Model View Presenter, nó bắt nguồn từ đầu những năm 1990 tại Talligent, một liên doanh của Apple, IBM và Hewlett-Packard. MVP là mô hình lập trình cơ bản để phát triển ứng dụng trong môi trường CommonPoint dựa trên C++ của Taligent. Sau này nó đã được Taligent chuyển sang Java.

    Đến năm 1998 thì Taligent giải thể, Andy Bower và Blair McFlashan của Dolphin Samlltalk đã điều chỉnh MVP để tạo cơ sở cho Smalltalk của họ.

    Đến năm 2006 thì Microsoft cũng bắt đầu kết hợp MVP vào tài liệu và ví dụ về lập trình giao diện người dùng trong .NET Framework.

    Đến nay thì MVP được sử dụng khá là rộng rãi vì những lợi ích mà nó đem lại cho các lập trình viên. Ngoài ra MVP cũng có rất nhiều biến thể để cải thiện những nhược điểm của nó.

    MVP là gì?

    MVP là một mẫu kiến trúc giao diện người dùng(user interface architecture pattern) được thiết kế để tạo điều kiện thuận lợi cho Automated Unit Testing(Chạy Unit Test tự động) và cải thiện việc phân tách các thành phần trong trình bày logic(presentation logic).

    MVP sinh ra dựa trên kiến trúc MVC, nó hướng tới mục tiêu cải thiện kiến trúc MVC.

    MVP được thể hiện băng hình ảnh sau:

    Model: là một interface xác định dữ liệu được hiển thị hoặc dữ liệu này được thực hiện trong giao diện người dùng.

    View: là một interface thụ động dùng để hiện thị dữ liệu của Model và định hướng các lệnh người dùng (events) tới Presenter để Presenter hành động dựa trên các dữ liệu đó.

    Presenter: hành động theo Model và View. Presenter lấy dữ liệu từ kho lưu trữ (Model), sau đó định dạng dữ liệu và hiển thị lên View.

    Ưu điểm của MVP

    Như đã nói ở trên do MVP được xây dựng dựa trên kiến trúc MVC nên nó sẽ có các ưu điểm tương tự như MVC. Các bạn có thể xem thêm về MVC ở bài viết sau: iOS Architecture Patterns: Cocoa MVC

    Mục đích cao cả của MVP sinh ra là để cải thiện những nhược điểm của kiến trúc MVC vì vậy nó giúp giảm tải lượng lớn logic nằm ở tầng Model so với mô hình MVC

    Kiến trúc MVP có tầng Presenter chuyên để xử lý các logic hiển thị, nó là thành phần trung gian tương tác với View và Model qua interface nên nó có thể viết Unit testing một cách dễ dàng.

    Nhược điểm của MVP

    Cũng như MVC, kiến trúc MVP cũng có những nhược điểm. Nhược điểm lớn nhất của MVP là càng về sau Presenter của MVP sẽ càng phình to nếu logic được thêm mới. Khí đó bạn sẽ rất khó để chia nhỏ khi presenter quá lớn.

    Biến thể MVP-C trong iOS

    Khái niệm Coordinator lần đầu tiên được đưa ra bởi Khanlou vào năm 2015, nó là một giải pháp để xử logic luồng cho View Controller.

    Dựa trên điều này kiến trúc MVP-C được ra đời với C là Coordinator làm nhiệm vụ xử lý luồng cho ứng dụng và các tầng cũ là Model, View và Presenter vẫn giống như MVP được mô tả ở trên.

    Ưu điểm của MVP-C

    View controller có thể tập trung vào mục tiêu chính của chúng. Giúp phân chia rõ ràng vai trò của View.

    Giúp giảm tải các logic trên các tầng khác, ta có thể đưa một số logic như phân luồng di chuyển màn hình từ presenter vào coordinator để giúp presenter đỡ trở nên cồng kềnh khi có quá nhiều logic. Nó đã cải thiện được nhược điểm của kiến trúc MVP truyền thống.

    Ngoài ra Coordinator cũng được ứng dụng vào các kiến trúc khác như MVC để tạo ra MVC-C và MVVM tạo ra MVVM-C.

    Tổng kết

    Đó là những kiến thức mà mình đã tích luỹ được khi làm việc với các dự án được thực hiện theo kiến trúc MVP. Mình hi vọng nó sẽ giúp ích cho các bạn khi cần thiết.