今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

iOSインターフェースの白黒実装

背景#

iOS APP インターフェースの白黒効果実現に関する調査整理。全体的に、現在オンラインには以下の方法があります:

  • H5 ウェブページ向け:js コードを注入
  • APP ネイティブインターフェース向け:
    • 画像と色を個別に設定
      • UIImageView のsetImageメソッドをフックし、UIImage のCategoryを追加してグレー画像を生成
      • UIColor のcolorWithRed:green:blue:alpha:メソッドをフック
    • インターフェース全体を処理
      • グレーの view を作成し、イベントを無視するように設定し、windowの最上層に追加
      • アプリ全体にグレーのフィルターを追加

具体的には以下の通りです:

実装#

ウェブページ向け:#

ウェブページの処理:

  • 基本クラスがある場合、基本クラスでWKWebviewを初期化する場所に以下のコードを追加:

        WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
        // jsスクリプト
        NSString *jScript = @"var filter = '-webkit-filter:grayscale(100%);-moz-filter:grayscale(100%); -ms-filter:grayscale(100%); -o-filter:grayscale(100%) filter:grayscale(100%);';document.getElementsByTagName('html')[0].style.filter = 'grayscale(100%)';";
        // 注入
        WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
        [config.userContentController addUserScript:wkUScript];
    
  • 基本クラスがない場合は、Swizzle_Methodを使用して実現:

    #import "WKWebView+Swizzle.h"
    
    @implementation WKWebView (Swizzle)
    
    + (void)load {
        Class class = [self class];
        SEL originalSelector = @selector(initWithFrame:configuration:);
        SEL swizzleSelector = @selector(swizzleInitWithFrame:configuration:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzleMethod = class_getInstanceMethod(class, swizzleSelector);
        
        method_exchangeImplementations(originalMethod, swizzleMethod);
    }
    
    - (instancetype)swizzleInitWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration {
        // jsスクリプト
        NSString *jScript = @"var filter = '-webkit-filter:grayscale(100%);-moz-filter:grayscale(100%); -ms-filter:grayscale(100%); -o-filter:grayscale(100%) filter:grayscale(100%);';document.getElementsByTagName('html')[0].style.filter = 'grayscale(100%)';";
        // 注入
        WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
                     
        WKUserContentController *wkUController = [[WKUserContentController alloc] init];
           [wkUController addUserScript:wkUScript];
        // 設定オブジェクト
        WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
        wkWebConfig.userContentController = wkUController;
        configuration = wkWebConfig;
        WKWebView *webView = [self swizzleInitWithFrame:frame configuration:configuration];
        return webView;
    }
    
    @end
    

APP ネイティブインターフェースの処理#

  • 色と画像の処理:

    a. 画像の処理:ほとんどの画像表示は最終的にUIImageViewsetImageメソッドを呼び出すため、このメソッドをフックし、表示前にグレーの画像を生成してから代入します。コードは以下の通り:

    UIImageViewsetImageメソッドをフック:

    
    #import "UIImageView+Swizzle.h"
    #import "UIImage+Category.h"
    
    @implementation UIImageView (Swizzle)
    
    + (void)load {
        Class class = [self class];
        SEL originalSelector = @selector(setImage:);
        SEL swizzleSelector = @selector(swizzleSetImage:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzleMethod = class_getInstanceMethod(class, swizzleSelector);
        
        method_exchangeImplementations(originalMethod, swizzleMethod);
    }
    
    - (void)swizzleSetImage:(UIImage *)image {
        UIImage *grayImage = [image anotherGrayImage];
        [self swizzleSetImage:grayImage];
    }
    
    @end
    
    

    グレー画像を生成するコードは以下の通り:

    
    #import <UIKit/UIKit.h>
    
    @interface UIImage (Category)
    
    // 使用は推奨しません。メモリ使用量が大きく、多画像リストをスクロールする際にパフォーマンスに影響を与え、カクつく可能性があります。
    //- (UIImage *)grayImage;
    
    // 推奨使用。メモリは相対的に小さく、カクつかず、画像にAチャネル(ARGBチャネル)が含まれているかに注意が必要です。
    - (UIImage *)anotherGrayImage;
    
    @end
    
    // 参考:https://blog.csdn.net/iOSxiaodaidai/article/details/113553395
    
    #import "UIImage+Category.h"
    
    @implementation UIImage (Category)
    
    - (UIImage *)grayImage {
        CIImage *beginImage = [CIImage imageWithCGImage:self.CGImage];
        CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"];
        [filter setValue:beginImage forKey:kCIInputImageKey];
        // 彩度を0に変更、範囲0-2、デフォルトは1
        [filter setValue:0 forKey:@"inputSaturation"];
        
        // フィルタリングされた画像を取得
        CIImage *outputImage = [filter outputImage];
        // 画像を変換し、GPUベースのCIContextオブジェクトを作成
        CIContext *context = [CIContext contextWithOptions:nil];
        CGImageRef cgImage = [context createCGImage:outputImage fromRect:[outputImage extent]];
        UIImage *newImage = [UIImage imageWithCGImage:cgImage];
        
        // Cオブジェクトを解放
        CGImageRelease(cgImage);
        return newImage;
    }
    
    - (UIImage *)anotherGrayImage {
        // ここで画像のサイズに注意
        CGFloat scale = [UIScreen mainScreen].scale;
        NSInteger width = self.size.width * scale;
        NSInteger height = self.size.height * scale;
        
        // 第一歩:色空間を作成——画像のグレースケール処理(グレースケール空間を作成)
        CGColorSpaceRef colorRef = CGColorSpaceCreateDeviceGray();
        
        // 第二歩:色空間のコンテキスト(画像データ情報を保存)
        CGContextRef context = CGBitmapContextCreate(nil, width, height, 8, 0, colorRef, kCGImageAlphaPremultipliedLast);
        
        // メモリを解放
        CGColorSpaceRelease(colorRef);
        if (context == nil) {
            return nil;
        }
        
        // 第三歩:画像をレンダリング(画像を描画)
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), self.CGImage);
        
        // 第四歩:描画された色空間をCGImageに変換(認識可能な画像タイプに変換)
        CGImageRef grayImageRef = CGBitmapContextCreateImage(context);
        
        // 第五歩:C/C++の画像CGImageをオブジェクト指向のUIImageに変換(iOSプログラムが認識する画像タイプに変換)
        UIImage *dstImage = [UIImage imageWithCGImage:grayImageRef];
        
        // メモリを解放
        CGContextRelease(context);
        CGImageRelease(grayImageRef);
        return dstImage;
    }
    
    @end
    
    

    しかし、プロジェクトを実行すると、プロジェクト内のキーボードの色も黒くなってしまうことに気づきます。なぜなら、キーボードも画像を使用しているため、置き換え前にキーボードの imageView であるかを判断し、そうであれば処理せず、そうでなければ黒色に置き換える必要があります。UIImageView+Swizzle.mの内容を以下のように修正します:

    
      #import "UIImageView+Swizzle.h"
      #import <objc/runtime.h>
      #import "UIImage+Category.h"
    
      @implementation UIImageView (Swizzle)
    
      + (void)load {
          Class class = [self class];
          SEL originalSelector = @selector(setImage:);
          SEL swizzleSelector = @selector(swizzleSetImage:);
          
          Method originalMethod = class_getInstanceMethod(class, originalSelector);
          Method swizzleMethod = class_getInstanceMethod(class, swizzleSelector);
          
          method_exchangeImplementations(originalMethod, swizzleMethod);
      }
    
      - (void)swizzleSetImage:(UIImage *)image {
          UIImage *grayImage = [image anotherGrayImage];
          
          // selfの最後のsuperViewを見つける
          UIView *superView = self;
          NSString *className = @"";
          while (superView.superview) {
              superView = superView.superview;
              className = NSStringFromClass([superView class]);
          }
          
          // 最後のsuperViewがキーボードウィンドウであれば、grayImageを設定しない
          if ([className containsString:@"UIRemoteKeyboardWindow"]) {
              [self swizzleSetImage:image];
          } else {
              [self swizzleSetImage:grayImage];
          }
      }
    
    

    再度実行して効果を確認すると、キーボードが正常に表示されることがわかります。

    b. 色の処理:

    すべての色の設定は最終的にUIColorcolorWithRed:green:blue:alpha:を通過するため、このメソッドをフックしてグレーの色を生成して返し表示します。コードは以下の通り:

    
      #import "UIColor+Swizzle.h"
    
      @implementation UIColor (Swizzle)
    
      + (void)load {
          Class class = [self class];
          SEL originalSelector = @selector(colorWithRed:green:blue:alpha:);
          SEL swizzleSelector = @selector(swizzle_colorWithRed:green:blue:alpha:);
          
          Method originalMethod = class_getClassMethod(class, originalSelector);
          Method swizzleMethod = class_getClassMethod(class, swizzleSelector);
          
          method_exchangeImplementations(originalMethod, swizzleMethod);
      }
    
      + (UIColor *)swizzle_colorWithRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha {
          CGFloat grayValue = 0.299*red + 0.587*green + 0.114*blue;
          UIColor *gray = [UIColor colorWithWhite:grayValue alpha:alpha];
          return gray;
      }
    
      @end
    
      ```
    
    
  • インターフェース全体の処理

    a. 方法 1:グレーの view を作成し、イベントを無視するように設定し、windowの最上層に追加

      #import <UIKit/UIKit.h>
    
      /// 最上位のビュー、フィルターを保持し、自身はタッチイベントを受け取らず、遮らない
      @interface UIViewOverLay : UIView
    
      @end
    
      #import "UIViewOverLay.h"
    
      @implementation UIViewOverLay
    
      - (instancetype)init {
          self = [super init];
          if (self) {
              [self setupSubviews];
          }
          return self;
      }
    
      - (instancetype)initWithFrame:(CGRect)frame {
          self = [super initWithFrame:frame];
          if (self) {
              [self setupSubviews];
          }
          return self;
      }
    
      - (void)setupSubviews {
          self.translatesAutoresizingMaskIntoConstraints = NO;
          self.backgroundColor = [UIColor lightGrayColor];
          self.layer.compositingFilter = @"saturationBlendMode";
      }
    
      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
          // クリックイベントを処理しない
          return nil;
      }
    
      @end
    
    

    b. 方法 2:アプリ全体にグレーのフィルターを追加し、同様にwindowの最上層に追加

    // RGBA色値を取得
    CGFloat r,g,b,a;
    [[UIColor lightGrayColor] getRed:&r green:&g blue:&b alpha:&a];
    // フィルターを作成
    id cls = NSClassFromString(@"CAFilter");
    id filter = [cls filterWithName:@"colorMonochrome"];
    // フィルターのパラメータを設定
    [filter setValue:@[@(r),@(g),@(b),@(a)] forKey:@"inputColor"];
    [filter setValue:@(0) forKey:@"inputBias"];
    [filter setValue:@(1) forKey:@"inputAmount"];
    // windowに設定
    window.layer.filters = [NSArray arrayWithObject:filter];
    

まとめ#

iOS APP インターフェースの白黒効果実現において、画像と色を個別に設定することは推奨されません。ほとんどの APP のホームページは H5 ではないため、グレーの view を作成し、イベントを無視するように設定し、グレーにしたいページまたは全体のwindowの最上層に追加することをお勧めします。

完全なコードは GitHub にあります:GrayTheme_iOS
CocoaPodsを通じてインストール可能:
pod 'GrayTheme'

参考#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。