ãã®è¨äºã¯Applibot Advent Calendar 2024ã®24æ¥ç®ã®è¨äºã§ãã
æ¥å¹´ããUnityã§ã²ã¼ã éçºãå§ããã®ã§ãæ¥ãã§åå¼·ä¸ã®äºåã§ããããã¾ã§ã¯iOSã¢ããªéçºãã¡ã¤ã³ã«æ¥åãè¡ã£ã¦ãã¾ããããUnityã§ãiOSã¢ããªéçºã®ç¥è¦ãæ´»ãããªããæ¤è¨ãã¦ãã¾ããä»åã¯ãã®ä¸é¨ã«ã¤ãã¦è§¦ãã¾ãã
iOSéçºã«ãããMVVM
以åã«éçºãã¦ããiOSã¢ããªã§ã¯ãMVVM + Clean Architectureããæ¡ç¨ããåã¬ã¤ã¤ã¼ãRxSwiftã§ç¹ãã§ãã¾ãããããããã¬ã¤ã¤ã¼ãç´°ããåãããã¨ã§ãã¤ã©ã¼ãã¬ã¼ãã³ã¼ããå¤ãçºçãã¦ãã¾ãã¾ããã
import RxRelay import RxSwift protocol ViewModelType { var event1: Observable<Void> { get } var event2: Observable<Void> { get } ... var eventN: Observable<Void> { get } } // ãã¤ã©ã¼ãã¬ã¼ãã³ã¼ããå¤æ°å«ãViewModel class ViewModel: ViewModelType { private let _event1 = PublishRelay<Void>() var event1: Observable<Void> { _event1.asObservable() } private let _event2 = PublishRelay<Void>() var event: Observable2<Void> { _event2.asObservable() } ... private let _eventN = PublishRelay<Void>() var eventN: Observable<Void> { _eventN.asObservable() } func doSomething() { _event1.accept(()) } }
Swiftã§ã¯propertyWrapperã¨ããæ©è½ãæ´»ç¨ãã¦ããã¤ã©ã¼ãã¬ã¼ãã³ã¼ããæé¤ãããã¨ãã§ãã¾ãã
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0258-property-wrappers.md
typealias PublishWrapper<T> = RelayWrapper<Observable<T>, T> @propertyWrapper struct RelayWrapper<Wrapped, Element> { let wrappedValue: Wrapped let accept: (Element) -> Void init(wrapped: Wrapped, accept: @escaping (Element) -> Void) { self.wrappedValue = wrapped self.accept = accept } } extension RelayWrapper where Wrapped == Observable<Element> { init() { let relay = PublishRelay<Element>() self.init(wrapped: relay.asObservable(), accept: { relay.accept($0) }) } } // propertyWrapperãæ´»ç¨ããViewModel class ViewModel: ViewModelType { @PublishWrapper() var event1: Observable<Void> @PublishWrapper() var event: Observable2<Void> ... @PublishWrapper() var eventN: Observable<Void> func do() { _event1.accept(()) } }
C#ã§MVVMãå®è£ ãã¦ã¿ã
C#ã§ãiOSéçºã¨åæ§ã«ãMVVM + Clean Architectureããå®è£ ãã¦ã¿ã¾ãã
using R3 interface IViewModel { Observable<Unit> Event1 { get; } Observable<Unit> Event2 { get; } ... Observable<Unit> EventN { get; } } // ãã¤ã©ã¼ãã¬ã¼ãã³ã¼ããå¤æ°å«ãViewModel class ViewModel: IViewModel { private readonly Subject<Unit> _event1 = new(); Observable<Unit> Event1 => _event1.AsObservable(); private readonly Subject<Unit> _event2 = new(); Observable<Unit> Event2 => _event2.AsObservable(); ... private readonly Subject<Unit> _eventN = new(); Observable<Unit> EventN => _eventN.AsObservable(); }
åæ§ã«ãã¤ã©ã¼ãã¬ã¼ãã³ã¼ããå¤æ°çã¾ãã¦ãã¾ãã®ã§ãC#ã«ããã¦ããã¤ã©ã¼ãã¬ã¼ãã³ã¼ããæé¤ãã¦ããã¾ããæåã¯Attributeãæ´»ç¨ãã¦å®ç¾ã§ããªããæ¤è¨ãã¾ããããGenericsãæ´»ç¨ã§ããªãã£ããããã¡ã¿ããã°ã©ãã³ã°ã§ã³ã¼ãçæãè¡ããã¨ã«ãã¾ããã
ããã¸ã§ã¯ãã®ã»ããã¢ãããªã©ã¯ @amenone_games ããã®ããã°ãåèã«ãªãã¾ããã https://qiita.com/amenone_games/items/762cbea245f95b212cfa
using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.DotnetRuntime.Extensions; using Microsoft.CodeAnalysis.Text; namespace ObservableWrapper; [Generator(LanguageNames.CSharp)] public class SourceGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { // èªåçæ対象ã®ããããã£ã«ä»ä¸ããObservableWrapperAttributeã®ã³ã¼ãçæ context.RegisterPostInitializationOutput(static x => SetAttribute(x)); // ObservableWrapperAttributeãä»ä¸ãããããããã£ã対象ã¨ããã³ã¼ãçæ var provider = context.SyntaxProvider.ForAttributeWithMetadataName ( context, "ObservableWrapperGenerator.ObservableWrapperAttribute", static (node, _) => node is VariableDeclaratorSyntax, static (cont, _) => cont ) .Combine(context.CompilationProvider); context.RegisterSourceOutput( context.CompilationProvider.Combine(provider.Collect()), static (sourceProductionContext, t) => { var (compilation, list) = t; var typeMetas = new List<SubjectTypeMeta>(); foreach (var (x, y) in list) { var typeMeta = SubjectTypeMeta.TryCreate(x.TargetSymbol, x.TargetNode); if (typeMeta != null) typeMetas.Add(typeMeta); } var generatedClassNames = new List<string>(); foreach(var typeMeta in typeMetas) { var fullClassName = typeMeta.GetFullClassName(); if (generatedClassNames.Contains(fullClassName)) continue; var commonTypeMetas = typeMetas .Where(x => x.GetFullClassName() == fullClassName) .ToList(); var builder = new StringBuilder(); var fileName = SubjectEmit.Emit(builder, commonTypeMetas); if (fileName != null) { // ã¡ã¿ããã°ã©ãã³ã°ã§åå¾ããæ å ±ãå ã«ã³ã¼ãçæ sourceProductionContext.AddSource( $"{fileName}.g.cs", SourceText.From(builder.ToString(), Encoding.UTF8) ); generatedClassNames.Add(fullClassName); } builder.Clear(); } }); } // èªåçæ対象ã®ããããã£ã«ä»ä¸ããObservableWrapperAttributeã®ã³ã¼ãçæ private static void SetAttribute(IncrementalGeneratorPostInitializationContext context) { const string attributeText = """ using System; namespace ObservableWrapperGenerator { [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] sealed class ObservableWrapperAttribute : Attribute { public ObservableWrapperAttribute() {} } } """; context.AddSource ( "ObservableWrapperAttribute.cs", SourceText.From(attributeText, Encoding.UTF8) ); } } // ã³ã¼ãçæã®å¯¾è±¡ // ViewModel.cs public partial class ViewModel: IViewModel { @ObservableWrapper private readonly Subject<Unit> _event1 = new(); @ObservableWrapper private readonly Subject<Unit> _event2 = new(); ... @ObservableWrapper private readonly Subject<Unit> _eventN = new(); } // ã³ã¼ãçæç© // ViewModel.g.cs public partial class VideModel { public Observable<Unit> Event1 => _event1.AsObservable(); public Observable<Unit> Event2 => _event2.AsObservable(); ... public Observable<Unit> EventN => _event3.AsObservable(); }
以ä¸ããµã³ãã«ã³ã¼ãã«ãªãã¾ãã https://github.com/Nonchalant/ObservableWrapper-CSharp
ã¾ã¨ã
ã¡ã¿ããã°ã©ãã³ã°ãæ´»ç¨ãããã¨ã§ãã³ã¼ãã®æ å ±ãå¹ççã«åå¾ã§ãããããã¢ã¤ãã¢æ¬¡ç¬¬ã§æ§ã ãªä½¿ãéãããã¨æãã¾ããããã ããçæããã³ã¼ããå¢ããã¨ããã©ã¼ãã³ã¹ã®ä½ä¸ãæ¸å¿µããããããç´°ããªãã¥ã¼ãã³ã°ãå¿ è¦ã«ãªãã¨æãã¾ãããã®è¨äºãã³ã¼ãçæã«é¢ããåèã«ãªãã°å¹¸ãã§ãã