본문 바로가기

[Flutter] admob(애드몹) - iOS 네이티브 광고 만들기 2탄 - xib파일, 스위프트 파일 만들기(ChatGPT로 오류 해결한 이야기)

ironwhale 2023. 4. 15.

1탄에서 iOS 앱을 만드는데 필요한 info.plist 파일 설정을 마쳤습니다. 이번에는 본격적으로 애드몹 네이티브 광고 송출을 위한 xib파일과 스위프트 파일을 소개하도록 하겠습니다. 아울러 ChatGPT로 오류도 해결한 이야기는 중간에 있으니 읽어봐 주세요.


xib 파일 만들기 

xib 파일이 무엇인지 chatGPT에게 물어보니 다음과 같이 대답 해주었습니다. 

XIB는 Interface Builder 파일로, iOS 애플리케이션에서 사용자 인터페이스를 디자인하고 구성하는 데 사용되는 파일 형식입니다. XIB 파일은 뷰 컨트롤러, 뷰 및 다른 사용자 인터페이스 요소를 포함할 수 있습니다. XIB 파일은 XML 형식으로 저장되며, Xcode에서 Interface Builder 툴을 통해 쉽게 수정할 수 있습니다. XIB 파일을 사용하면 코드를 작성하지 않고도 사용자 인터페이스를 만들고 연결할 수 있습니다. 따라서 XIB 파일은 iOS 애플리케이션 개발에서 자주 사용됩니다.

 

xib은 iOS에서 UI를 구성하는 xml 형식의 파일이었네요. 그럼 지금부터 xib파일은 만들어 보겠습니다. 

 

플러터 프로젝트 폴더에 있는 ios/Runner로 가셔서 우선 ListTileNativeAdView.xib 이렇게 파일을 만듭니다. 그리고 아래와 같은 코드를 그냥 입력 합니다. 코드의 출처는 맨아래 표시하도록 하겠습니다. 

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
    <device id="retina6_72" orientation="portrait" appearance="light"/>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
        <capability name="System colors in document resources" minToolsVersion="11.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <objects>
        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
        <view contentMode="scaleToFill" id="iN0-l3-epB" customClass="GADNativeAdView">
            <rect key="frame" x="0.0" y="0.0" width="430" height="932"/>
            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
            <subviews>
                <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="aDH-L2-ViC" userLabel="icon">
                    <rect key="frame" x="16" y="442" width="48" height="48"/>
                    <constraints>
                        <constraint firstAttribute="width" constant="48" id="Ii0-QX-7Bv"/>
                        <constraint firstAttribute="height" constant="48" id="bLX-V9-GK7"/>
                    </constraints>
                </imageView>
                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Ad" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="FVf-yB-1vd" userLabel="attribution">
                    <rect key="frame" x="0.0" y="0.0" width="16" height="15"/>
                    <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                    <color key="backgroundColor" systemColor="systemOrangeColor"/>
                    <fontDescription key="fontDescription" type="system" pointSize="12"/>
                    <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                    <nil key="highlightedColor"/>
                </label>
                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AF1-3a-kBu" userLabel="headline">
                    <rect key="frame" x="80" y="446" width="314" height="19.666666666666686"/>
                    <fontDescription key="fontDescription" type="system" pointSize="16"/>
                    <nil key="textColor"/>
                    <nil key="highlightedColor"/>
                </label>
                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SqM-pP-iR7" userLabel="body">
                    <rect key="frame" x="80" y="469" width="314" height="17"/>
                    <fontDescription key="fontDescription" type="system" pointSize="14"/>
                    <color key="textColor" systemColor="systemGrayColor"/>
                    <nil key="highlightedColor"/>
                </label>
            </subviews>
            <color key="backgroundColor" red="0.98039871450000005" green="0.98038035629999998" blue="0.98039287330000002" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
            <constraints>
                <constraint firstAttribute="trailingMargin" secondItem="SqM-pP-iR7" secondAttribute="trailing" constant="16" id="WZu-qp-hh3"/>
                <constraint firstItem="SqM-pP-iR7" firstAttribute="bottom" secondItem="aDH-L2-ViC" secondAttribute="bottom" constant="-4" id="Yeo-Tn-OgQ"/>
                <constraint firstItem="aDH-L2-ViC" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="aY2-Z1-3G7"/>
                <constraint firstItem="AF1-3a-kBu" firstAttribute="top" secondItem="aDH-L2-ViC" secondAttribute="top" constant="4" id="mwE-qj-92K"/>
                <constraint firstItem="aDH-L2-ViC" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16.000000000000114" id="n9m-Dv-DAB"/>
                <constraint firstAttribute="trailingMargin" secondItem="AF1-3a-kBu" secondAttribute="trailing" constant="16" id="pog-io-Jsp"/>
                <constraint firstItem="SqM-pP-iR7" firstAttribute="leading" secondItem="aDH-L2-ViC" secondAttribute="trailing" constant="16" id="wqd-KX-n5Y"/>
                <constraint firstItem="AF1-3a-kBu" firstAttribute="leading" secondItem="aDH-L2-ViC" secondAttribute="trailing" constant="16" id="xaD-rv-wJ2"/>
            </constraints>
            <connections>
                <outlet property="bodyView" destination="SqM-pP-iR7" id="X3G-1T-dMs"/>
                <outlet property="headlineView" destination="AF1-3a-kBu" id="FCc-oH-TDT"/>
                <outlet property="iconView" destination="aDH-L2-ViC" id="VUa-bH-mKt"/>
            </connections>
            <point key="canvasLocation" x="137.68115942028987" y="152.44565217391306"/>
        </view>
    </objects>
    <resources>
        <systemColor name="systemGrayColor">
            <color red="0.55686274509803924" green="0.55686274509803924" blue="0.57647058823529407" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
        </systemColor>
        <systemColor name="systemOrangeColor">
            <color red="1" green="0.58431372549019611" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
        </systemColor>
    </resources>
</document>

네이티브 광고가 표시되는 UI를 만드는 xib파일은 끝났습니다. 다음은 xib파일과 연결하는 스위프트 파일인 ListTileNativeAdFactory.swift 파일은 만들어 보겠습니다. 


ListTileNativeAdFactory.swift 만들기 

마찬가지로 같은 폴더(ios/Runner)로 가셔서 우선 ListTileNativeAdFactory.swift 이렇게 파일을 만듭니다. 그리고 아래 코드를 입력합니다. 처음에 구글 공식을 사이트에서 그래도 복붙 했는데 오류가 나서 스위프트 코드를 모르는데 어쩌지 했는데 chatGPT를 통해 해당 코드를 입력하네 알아서 수정을 해주었습니다. 이제 ChatGPT를 잘 활요하는 사람과 못하는 사람과의 생산성의 차이는 어마어마 해질 것이라는 생각이 절로 드네요

 

ListTileNativeAdFactory.swift 코드 

이 코드에서 "ListTileNativeAdView" 이것은 xib 파일 이름과 똑같이 해야 합니다. 

Bundle.main.loadNibNamed("ListTileNativeAdView", owner: nil, options: nil)!.first

전체 코드 

import google_mobile_ads


class ListTileNativeAdFactory : FLTNativeAdFactory {

    func createNativeAd(_ nativeAd: GADNativeAd,
                        customOptions: [AnyHashable : Any]? = nil) -> GADNativeAdView? {
        let nibView = Bundle.main.loadNibNamed("ListTileNativeAdView", owner: nil, options: nil)!.first
        let nativeAdView = nibView as! GADNativeAdView

        (nativeAdView.headlineView as! UILabel).text = nativeAd.headline

        (nativeAdView.bodyView as? UILabel)?.text = nativeAd.body
        nativeAdView.bodyView?.isHidden = nativeAd.body == nil

        
        (nativeAdView.iconView as? UIImageView)?.image = nativeAd.icon?.image
        nativeAdView.iconView?.isHidden = nativeAd.icon == nil

        nativeAdView.callToActionView?.isUserInteractionEnabled = false

        nativeAdView.nativeAd = nativeAd

        return nativeAdView
    }
}

에러도 수정해주는 chatGPT

챗지피티&#44; chatGPT


AppDelegate.swift 수정하기 

이제 마지막 단계입니다. 플러터와 스위프트 코드를 연결하는 부분이라 할 수 있는데요. 안드로이드와 마찬가지로 factoryId를 통해 플러터와 스위프트 코드를 연결합니다. 안드로이드와 똑같은 아이디를 사용하면 플러터 코드 변경없이 안드로이드와 iOS 둘다 편하게 사용이 가능합니다. 

 

return 전에 무언가 아이디를 입력하는 부분이 있는데 아마 실행하면 에러와 함께 로그에 아이디가 뜰텐데 그것을 입력하시면 됩니다. 

 

추가된 코드 

let listTileFactory = ListTileNativeAdFactory()
    FLTGoogleMobileAdsPlugin.registerNativeAdFactory(
    self, factoryId:"listTile",nativeAdFactory:listTileFactory)

    GADMobileAds.sharedInstance().requestConfiguration.testDeviceIdentifiers = [ "아이디를 입력하세요" ]

전체 코드

import UIKit
import Flutter

import google_mobile_ads

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    let listTileFactory = ListTileNativeAdFactory()
    FLTGoogleMobileAdsPlugin.registerNativeAdFactory(
    self, factoryId:"listTile",nativeAdFactory:listTileFactory)

    GADMobileAds.sharedInstance().requestConfiguration.testDeviceIdentifiers = [ "아이디를 입력하세요" ]

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

 

이제 네이티브 광고를 위한 iOS 부분 설정은 끝났습니다. 이제 플러터 코드로 넘어가겠습니다. 


플러터 코드 부분

이전에 만든 안드로이드에서 네이티브 광고 구현하기에 플러터 위젯 부분과 똑같습니다. 

import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';

import '../util/logger.dart';
import 'ad_id.dart';

class NativeAds extends StatefulWidget {
  NativeAds({Key? key}) : super(key: key);

  @override
  State<NativeAds> createState() => _NativeAdsState();
}

class _NativeAdsState extends State<NativeAds> {
  late NativeAd _ad;
  TargetPlatform? os;
  bool isLoaded = false;

  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
    os = Theme.of(context).platform;
    logger.d("native didChangeDependencies");
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    _ad.dispose();
  }

  @override
  void initState() {
    super.initState();

    logger.d("native init");
    _ad = NativeAd(
      adUnitId: NATIVE_ID[os == TargetPlatform.iOS ? 'ios' : 'android']!,
      factoryId: "adFactoryExample",
      request: const AdRequest(),
      listener: NativeAdListener(onAdLoaded: (ad) {
        setState(() {
          _ad = ad as NativeAd;
          isLoaded = true;
        });
      }, onAdFailedToLoad: (ad, error) {
        ad.dispose();
      }),
    );
    _ad.load();
  }

  @override
  Widget build(BuildContext context) {
    if (isLoaded == true) {
      logger.d("native build");
      return SizedBox(height: 60, child: AdWidget(ad: _ad));
    } else {
      return const SizedBox(height: 0);
    }
  }
}

iOS에서 구현된 네이티브 광고 모습

깔끔하게 구현이 되었습니다. 사실 이런식으로 네이티브 광고를 사용할꺼면 그냥 배너 광고를 하는것이 생산성이 높을거 같다는 생각은 들지만 xib이나 xml을 통한 UI 만들기에 익숙해 진다면 사람들에게 거부감이 적은 광고 송출도 가능하지 않을까 싶습니다. 몬가 코드만 단순히 나열한거 같아 조금은 찜찜하지만 추후 완전히 제것으로 만들어서 더 자세한 내용의 포스팅이 되도록 해보겠습니다. 

012


출처 모음

출처1 : xib 코드 

 

GitHub - googlecodelabs/admob-inline-ads-in-flutter

Contribute to googlecodelabs/admob-inline-ads-in-flutter development by creating an account on GitHub.

github.com

 

 

댓글